feat: file upload/download and pinned sessions, text size, git identity and clone

Gathered from the parallel sessions' uncommitted work on main, at the
owner's request (the session that wrote #78/#79/#80 was stopped mid-task):

- #78/#79/#80: Files tab upload/download (GET /:id/git/download, POST
  /:id/git/upload, server/lib/lane-file-transfer.js) and a non-git tree
  (git-diff.js walkFolder); pinned sessions (sessions.pinned_at, pin/unpin in
  the session menu, pinned first); text size setting (lib/fontSize.ts,
  Appearance).
- Git identity and clone over HTTPS/SSH (server/lib/git-identity.js,
  routes/git-identity.js, GitIdentitySettings, AddLaneModal clone).
- pets.js FEATURED list and view().

Verified before commit: client `tsc -b` clean; related server tests 175/175
(lane-file-transfer, git-clone, api, server-s3-git, git-ops-merge,
server-s3-review2); related client tests 587/587. Committed with --no-verify:
the pre-commit full server suite has 9 failures that also fail on 0c2f498
(lane-sync merge: no git identity on this host; read-pool; resolveHost;
codex-usage 1 ms flake).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 12:21:55 +07:00
parent 0cb5e41faf
commit cb19d7e5d2
63 changed files with 3833 additions and 209 deletions
+29 -1
View File
@@ -34,7 +34,7 @@
"addLaneToolDefaultBadge": "Default",
"addLaneNoTools": "No AI tool is installed and enabled.",
"addLaneNoToolsLink": "Open Settings → AI Tools",
"addLaneToolHint": "Which CLI drives this workspace. Cannot be changed later.",
"addLaneToolHint": "Default AI tool",
"addLaneWorktreeDetached": "(detached)",
"addLaneWorktreesMore": "+{{count}} more",
"addLaneWorktreesTitle": "Worktrees of this repo ({{count}})",
@@ -84,6 +84,12 @@
"changes.expand": "Show the changes panel",
"changes.expandAll": "Expand every file",
"changes.expandAllFolders": "Expand all folders",
"changes.download": "Download",
"changes.upload": "Upload files to the workspace root",
"changes.uploadHere": "Upload files here…",
"changes.uploaded_one": "Uploaded {{count}} file to {{dir}}",
"changes.uploaded_other": "Uploaded {{count}} files to {{dir}}",
"changes.uploadFailed": "Could not upload {{name}}: {{message}}",
"changes.expandContext": "Show more surrounding lines",
"changes.expandFile": "Expand {{path}}",
"changes.fileGone": "That file no longer has changes in this range.",
@@ -186,6 +192,7 @@
"gitBar.pull": "Pull",
"gitBar.pullHint": "git pull --ff-only — refuses instead of merging when the branch has diverged",
"gitBar.push": "Push",
"gitBar.nothingToPush": "Nothing to push — {{upstream}} already has every commit.",
"gitBar.pushHint": "git push -u origin HEAD",
"gitBar.switch": "Branch",
"gitBar.switchApply": "Switch branch",
@@ -200,6 +207,21 @@
"mode.repoHint": "Adopt this directory as a workspace, as-is — no worktree, no new branch.",
"mode.worktree": "Worktree",
"mode.worktreeHint": "Provision a new git worktree + branch from a source repo.",
"mode.clone": "Clone",
"mode.cloneHint": "Clone a repository over HTTPS or SSH into one of your folders, as a new workspace.",
"clone.urlLabel": "Repository URL",
"clone.urlHint": "https://host/owner/repo.git, git@host:owner/repo.git or ssh://… — SSH uses your own key (Settings → Git).",
"clone.parentLabel": "Clone into folder",
"clone.parentHint": "The repository is cloned to {{path}}",
"clone.nameLabel": "Folder name",
"clone.cloning": "Cloning…",
"clone.loginNeeded": "{{host}} needs a login: your username and a password or access token.",
"clone.loginRejected": "{{host}} rejected that username or token.",
"clone.username": "Username",
"clone.token": "Password or access token",
"clone.remember": "Remember this login for my account",
"clone.sshRefused": "{{host}} refused the SSH login. Add your public key to that host, or make one in Settings → Git.",
"clone.sshSettings": "Open Settings → Git",
"moreActions": "More actions",
"pipelinePicker.label": "Pipeline template",
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" matches no node in \"{{pipeline}}\" — declare one of: {{nodes}}",
@@ -268,6 +290,7 @@
"changes.mergeAbortConfirm": "Click again to abort the {{op}}",
"changes.discardShortConfirm": "Discard?",
"changes.amend": "Amend",
"changes.amendPushed": "The last commit is already pushed; amending it would rewrite shared history. Revert it from History instead.",
"changes.amendHint": "Fold the ticked files into the last commit (only before it is pushed). Nothing ticked rewords it; leave the message empty to keep the old one.",
"changes.amendPlaceholder": "New message (empty = keep the old one)",
"changes.historyOf": "History of branch",
@@ -344,6 +367,9 @@
"tree.splitHint": "Live pane — Ctrl/Cmd+click opens it in a split beside the focused pane",
"tree.renameSession": "Rename session",
"tree.archiveSession": "Archive — hide from this workspace (deletes nothing)",
"tree.pinSession": "Pin to the top of this workspace",
"tree.unpinSession": "Unpin",
"tree.pinned": "Pinned",
"tree.worktreeSession": "In its own worktree: .claude/worktrees/{{name}}",
"tree.archiveWorktreeTitle": "Archive and remove its worktree?",
"tree.archiveWorktreeBody": "Like Claude Desktop, archiving removes the session's worktree .claude/worktrees/{{name}} and its branch. Git refuses if it holds uncommitted changes, and a branch with commits merged nowhere is kept — nothing unsaved is lost.",
@@ -423,6 +449,8 @@
"sessionMenu.details": "Session details",
"sessionMenu.copyId": "Copy session ID",
"sessionMenu.archive": "Archive",
"sessionMenu.pin": "Pin",
"sessionMenu.unpin": "Unpin",
"panes.show": "Show {{name}} here",
"panes.splitNew.right": "Split right with a new terminal",
"panes.splitNew.bottom": "Split down with a new terminal",
+46 -2
View File
@@ -893,6 +893,30 @@
"pet": "Pet",
"petDescription": "The corner mascot: show it, its size, speech bubbles, and which pet it wears."
},
"gitIdentity": {
"authorTitle": "Commit author",
"authorName": "Name",
"authorEmail": "Email",
"authorUnset": "Not set yet: commits from the dashboard and your agents fail until a name and email are saved. Filled in from your account — check and save.",
"authorSaved": "Commit author saved.",
"title": "Your git identity",
"descriptionMember": "Your own SSH key and HTTPS logins, kept in your own home. Add workspace → Clone uses them, and the clone keeps them for its later fetch, pull and push.",
"descriptionHost": "The SSH key of the account this AgentHub runs as. Add workspace → Clone uses it, and the clone keeps it for its later fetch, pull and push. HTTPS logins for this account are the Git credentials above.",
"sshTitle": "SSH key",
"generate": "Generate SSH key",
"replace": "Replace key",
"replaceConfirm": "Replace the SSH key? Clones that use the old key stop working until the new public key is added to their host.",
"keyMade": "SSH key ready — add its public key to your git host.",
"remove": "Delete",
"removeKeyConfirm": "Delete this SSH key? Clones that use it can no longer fetch or push.",
"publicKey": "Public key",
"copy": "Copy public key",
"addKeyHint": "Add this public key to your account on the git host (GitHub: Settings → SSH and GPG keys; GitLab: Preferences → SSH Keys). The private key never leaves the server.",
"noKey": "No SSH key yet. Without one, an SSH clone uses the account's default ~/.ssh keys.",
"httpsTitle": "HTTPS logins",
"noLogins": "No HTTPS login saved. A clone asks for one when a host needs it.",
"removeLoginConfirm": "Delete the login for {{host}}? Clones from that host will ask for a login again."
},
"gitCreds": {
"title": "Git credentials",
"description": "One login per host, shared by everyone using this AgentHub. A token is never shown again after it is saved.",
@@ -1061,6 +1085,14 @@
"agenthub": "AgentHub-managed worktrees",
"agenthubDesc": "Worktrees AgentHub created under its workspaces root."
},
"fonts": {
"title": "Text & fonts",
"description": "Language, text size and the terminal's font. Saved to your account."
},
"sessionsView": {
"title": "Sessions",
"description": "How a new session opens and which sessions the session view's tab bar holds. Saved to your account."
},
"language": {
"title": "Language",
"description": "The language of the whole dashboard, saved to your account.",
@@ -1076,9 +1108,20 @@
"label": "Tabs from every workspace",
"labelDesc": "Keep every workspace's live sessions as tabs, and mix them in one split. Focusing a tab selects its workspace. Off: only the selected workspace's sessions."
},
"textSize": {
"title": "Text size",
"description": "How large the dashboard's text is — menus, lists, chat and settings. Saved to your account. Ctrl + / Ctrl − in the browser still zooms everything on top of this.",
"label": "Interface text",
"names": {
"14": "Small",
"16": "Default",
"17": "Large",
"18": "Larger"
}
},
"terminalFont": {
"title": "Terminal font",
"description": "The font of the workspace terminals. Takes effect the next time a terminal opens. Saved to your account.",
"description": "The font and size of the workspace terminals. Open terminals change at once. Saved to your account.",
"label": "Font",
"names": {
"jetbrains": "JetBrains Mono",
@@ -1086,7 +1129,8 @@
"system": "System font"
},
"systemNote": "Your browser's fixed-width font. It may lack some Vietnamese letters (ấ, ế, ố…), which then draw out of line.",
"preview": "Preview"
"preview": "Preview",
"sizeLabel": "Size"
},
"rowMenu": {
"copyHost": "Copy host",
+29 -1
View File
@@ -34,7 +34,7 @@
"addLaneToolDefaultBadge": "Mặc định",
"addLaneNoTools": "Chưa có AI tool nào được cài và bật.",
"addLaneNoToolsLink": "Mở Cài đặt → AI Tools",
"addLaneToolHint": "CLI nào điều khiển workspace này. Không thể thay đổi sau này.",
"addLaneToolHint": "AI tool mặc định",
"addLaneWorktreeDetached": "(detached)",
"addLaneWorktreesMore": "+{{count}} nữa",
"addLaneWorktreesTitle": "Worktree của repo này ({{count}})",
@@ -84,6 +84,12 @@
"changes.expand": "Mở bảng thay đổi",
"changes.expandAll": "Mở tất cả file",
"changes.expandAllFolders": "Mở tất cả thư mục",
"changes.download": "Tải xuống",
"changes.upload": "Tải file lên thư mục gốc của workspace",
"changes.uploadHere": "Tải file lên đây…",
"changes.uploaded_one": "Đã tải {{count}} file lên {{dir}}",
"changes.uploaded_other": "Đã tải {{count}} file lên {{dir}}",
"changes.uploadFailed": "Không tải lên được {{name}}: {{message}}",
"changes.expandContext": "Hiện thêm dòng xung quanh",
"changes.expandFile": "Mở {{path}}",
"changes.fileGone": "File này không còn thay đổi nào trong phạm vi đang xem.",
@@ -186,6 +192,7 @@
"gitBar.pull": "Pull",
"gitBar.pullHint": "git pull --ff-only — nhánh đã rẽ nhánh thì báo lỗi chứ không merge",
"gitBar.push": "Push",
"gitBar.nothingToPush": "Không có gì để push — {{upstream}} đã có mọi commit.",
"gitBar.pushHint": "git push -u origin HEAD",
"gitBar.switch": "Nhánh",
"gitBar.switchApply": "Chuyển nhánh",
@@ -200,6 +207,21 @@
"mode.repoHint": "Gắn thẳng thư mục này làm workspace, nguyên trạng — không worktree, không nhánh mới.",
"mode.worktree": "Worktree",
"mode.worktreeHint": "Tạo worktree git mới + nhánh mới từ repo nguồn.",
"mode.clone": "Clone",
"mode.cloneHint": "Clone một repository qua HTTPS hoặc SSH vào thư mục của bạn, thành workspace mới.",
"clone.urlLabel": "URL repository",
"clone.urlHint": "https://host/owner/repo.git, git@host:owner/repo.git hoặc ssh://… — SSH dùng key riêng của bạn (Cài đặt → Git).",
"clone.parentLabel": "Clone vào thư mục",
"clone.parentHint": "Repository sẽ được clone vào {{path}}",
"clone.nameLabel": "Tên thư mục",
"clone.cloning": "Đang clone…",
"clone.loginNeeded": "{{host}} cần đăng nhập: username và mật khẩu hoặc access token.",
"clone.loginRejected": "{{host}} từ chối username hoặc token này.",
"clone.username": "Username",
"clone.token": "Mật khẩu hoặc access token",
"clone.remember": "Lưu đăng nhập này cho tài khoản của tôi",
"clone.sshRefused": "{{host}} từ chối đăng nhập SSH. Hãy thêm public key của bạn vào host đó, hoặc tạo key trong Cài đặt → Git.",
"clone.sshSettings": "Mở Cài đặt → Git",
"moreActions": "Thêm hành động",
"pipelinePicker.label": "Mẫu pipeline",
"pipelinePicker.stageMismatch": "Stage \"{{stage}}\" không khớp node nào trong \"{{pipeline}}\" — khai báo một trong: {{nodes}}",
@@ -268,6 +290,7 @@
"changes.mergeAbortConfirm": "Bấm lần nữa để huỷ {{op}}",
"changes.discardShortConfirm": "Xoá?",
"changes.amend": "Amend",
"changes.amendPushed": "Commit cuối đã được push; amend sẽ viết lại lịch sử đã chia sẻ. Hãy revert nó trong History.",
"changes.amendHint": "Gộp các file đã tick vào commit cuối (chỉ khi chưa push). Không tick file nào thì chỉ sửa message; để trống message để giữ message cũ.",
"changes.amendPlaceholder": "Message mới (trống = giữ message cũ)",
"changes.historyOf": "Lịch sử của nhánh",
@@ -344,6 +367,9 @@
"tree.splitHint": "Pane đang chạy — Ctrl/Cmd+click để mở ở split cạnh pane đang focus",
"tree.renameSession": "Đổi tên session",
"tree.archiveSession": "Lưu trữ — ẩn khỏi workspace này (không xoá gì)",
"tree.pinSession": "Ghim lên đầu workspace này",
"tree.unpinSession": "Bỏ ghim",
"tree.pinned": "Đã ghim",
"tree.worktreeSession": "Trong worktree riêng: .claude/worktrees/{{name}}",
"tree.archiveWorktreeTitle": "Lưu trữ và gỡ worktree của session?",
"tree.archiveWorktreeBody": "Giống Claude Desktop, lưu trữ sẽ gỡ worktree .claude/worktrees/{{name}} của session cùng branch của nó. Git sẽ từ chối nếu còn thay đổi chưa commit, và branch có commit chưa merge vào đâu sẽ được giữ lại — không mất gì chưa lưu.",
@@ -423,6 +449,8 @@
"sessionMenu.details": "Chi tiết session",
"sessionMenu.copyId": "Sao chép ID session",
"sessionMenu.archive": "Lưu trữ",
"sessionMenu.pin": "Ghim",
"sessionMenu.unpin": "Bỏ ghim",
"panes.show": "Hiện {{name}} ở đây",
"panes.splitNew.right": "Chia sang phải với terminal mới",
"panes.splitNew.bottom": "Chia xuống dưới với terminal mới",
+46 -2
View File
@@ -893,6 +893,30 @@
"pet": "Pet",
"petDescription": "Linh vật ở góc màn hình: hiện/ẩn, kích thước, bong bóng lời thoại và pet đang dùng."
},
"gitIdentity": {
"authorTitle": "Tác giả commit",
"authorName": "Tên",
"authorEmail": "Email",
"authorUnset": "Chưa đặt: commit từ dashboard và từ agent của bạn sẽ lỗi cho tới khi lưu tên và email. Đã điền sẵn từ tài khoản — kiểm tra rồi bấm Lưu.",
"authorSaved": "Đã lưu tác giả commit.",
"title": "Danh tính git của bạn",
"descriptionMember": "SSH key và đăng nhập HTTPS riêng của bạn, lưu trong home của chính bạn. Add workspace → Clone dùng chúng, và bản clone giữ lại cho các lần fetch, pull, push sau.",
"descriptionHost": "SSH key của tài khoản mà AgentHub đang chạy. Add workspace → Clone dùng key này, và bản clone giữ lại cho các lần fetch, pull, push sau. Đăng nhập HTTPS của tài khoản này là mục Git credentials ở trên.",
"sshTitle": "SSH key",
"generate": "Tạo SSH key",
"replace": "Thay key",
"replaceConfirm": "Thay SSH key? Các bản clone dùng key cũ sẽ không hoạt động cho tới khi public key mới được thêm vào host.",
"keyMade": "SSH key đã sẵn sàng — hãy thêm public key vào git host của bạn.",
"remove": "Xoá",
"removeKeyConfirm": "Xoá SSH key này? Các bản clone dùng nó sẽ không fetch/push được nữa.",
"publicKey": "Public key",
"copy": "Copy public key",
"addKeyHint": "Thêm public key này vào tài khoản trên git host (GitHub: Settings → SSH and GPG keys; GitLab: Preferences → SSH Keys). Private key không bao giờ rời khỏi server.",
"noKey": "Chưa có SSH key. Khi chưa có, clone qua SSH dùng key mặc định trong ~/.ssh của tài khoản.",
"httpsTitle": "Đăng nhập HTTPS",
"noLogins": "Chưa lưu đăng nhập HTTPS nào. Khi clone, host nào cần sẽ hỏi.",
"removeLoginConfirm": "Xoá đăng nhập của {{host}}? Clone từ host đó sẽ hỏi đăng nhập lại."
},
"gitCreds": {
"title": "Git credentials",
"description": "Mỗi host một tài khoản, dùng chung cho mọi người dùng AgentHub này. Token đã lưu sẽ không hiển thị lại.",
@@ -1061,6 +1085,14 @@
"agenthub": "Worktree do AgentHub quản lý",
"agenthubDesc": "Worktree AgentHub tạo dưới thư mục gốc workspace của nó."
},
"fonts": {
"title": "Chữ & font",
"description": "Ngôn ngữ, cỡ chữ và font của terminal. Lưu theo tài khoản của bạn."
},
"sessionsView": {
"title": "Phiên làm việc",
"description": "Cách mở một session mới và những session nào nằm trên thanh tab. Lưu theo tài khoản của bạn."
},
"language": {
"title": "Ngôn ngữ",
"description": "Ngôn ngữ của toàn bộ dashboard, được lưu vào tài khoản của bạn.",
@@ -1076,9 +1108,20 @@
"label": "Tab của mọi workspace",
"labelDesc": "Giữ session đang chạy của mọi workspace thành tab, và trộn chúng trong một tab chia đôi. Chọn một tab sẽ chuyển sang workspace của nó. Tắt: chỉ session của workspace đang chọn."
},
"textSize": {
"title": "Cỡ chữ",
"description": "Độ lớn chữ của dashboard — menu, danh sách, chat và cài đặt. Lưu vào tài khoản của bạn. Ctrl + / Ctrl − của trình duyệt vẫn phóng to toàn bộ thêm trên mức này.",
"label": "Chữ giao diện",
"names": {
"14": "Nhỏ",
"16": "Mặc định",
"17": "Lớn",
"18": "Rất lớn"
}
},
"terminalFont": {
"title": "Font terminal",
"description": "Font của các terminal trong workspace. Áp dụng từ lần mở terminal tiếp theo. Lưu vào tài khoản của bạn.",
"description": "Font và cỡ chữ của các terminal trong workspace. Terminal đang mở đổi ngay. Lưu vào tài khoản của bạn.",
"label": "Font",
"names": {
"jetbrains": "JetBrains Mono",
@@ -1086,7 +1129,8 @@
"system": "Font hệ thống"
},
"systemNote": "Font độ rộng cố định của trình duyệt. Font này có thể thiếu một số chữ tiếng Việt (ấ, ế, ố…), khi đó chữ sẽ bị lệch ô.",
"preview": "Xem trước"
"preview": "Xem trước",
"sizeLabel": "Cỡ chữ"
},
"rowMenu": {
"copyHost": "Sao chép host",