Compare commits
45 Commits
v0.12.3-de
...
codenomad/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc554ef98 | ||
|
|
46150cda5e | ||
|
|
0874f78ccf | ||
|
|
88da377795 | ||
|
|
3533dabda0 | ||
|
|
25555ed42c | ||
|
|
df6c96453f | ||
|
|
db3a786b48 | ||
|
|
1e47389df3 | ||
|
|
d7ae575042 | ||
|
|
8346b7b631 | ||
|
|
c441d7d3ce | ||
|
|
be8fcc98c5 | ||
|
|
658253a3fd | ||
|
|
0e96662a07 | ||
|
|
eb77c06571 | ||
|
|
a6cb70ed41 | ||
|
|
13596e8082 | ||
|
|
d9d56d77bc | ||
|
|
c886344e2f | ||
|
|
69cb049a39 | ||
|
|
38cdb4ddb1 | ||
|
|
b11a9e3ec8 | ||
|
|
268d23e9f6 | ||
|
|
f266577c75 | ||
|
|
3bad0afd7d | ||
|
|
8567d49178 | ||
|
|
09284ee2ce | ||
|
|
a2e30f1b54 | ||
|
|
a4af811de3 | ||
|
|
c5aa59ca75 | ||
|
|
b8e0714b68 | ||
|
|
3f890e5de1 | ||
|
|
935926d875 | ||
|
|
74f753abf4 | ||
|
|
d15340a4b8 | ||
|
|
108cad82d0 | ||
|
|
823dd2d687 | ||
|
|
313e82880b | ||
|
|
68407a01a4 | ||
|
|
0283493f2a | ||
|
|
e989795de3 | ||
|
|
103d2bf1a8 | ||
|
|
0ce7a47e03 | ||
|
|
5df8809c82 |
147
.github/workflows/comment-pr-artifacts.yml
vendored
147
.github/workflows/comment-pr-artifacts.yml
vendored
@@ -1,88 +1,105 @@
|
|||||||
name: Comment PR Artifacts
|
name: Comment PR Artifacts
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
pull_request_target:
|
||||||
workflows:
|
|
||||||
- PR Build Validation
|
|
||||||
types:
|
types:
|
||||||
- completed
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
pull-requests: write
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
comment:
|
comment:
|
||||||
# Only runs for PR Build Validation runs triggered by PRs.
|
|
||||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||||
|
IS_DRAFT: ${{ github.event.pull_request.draft }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
RETENTION_DAYS: 7
|
||||||
steps:
|
steps:
|
||||||
- name: Comment with artifact download link
|
- name: Check PR authorization
|
||||||
uses: actions/github-script@v7
|
id: auth
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "$BASE_REF" = "dev" ]; then
|
||||||
|
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
normalized=",${ALLOWED_ACTORS},"
|
||||||
|
if [[ "$normalized" == *",${ACTOR},"* ]]; then
|
||||||
|
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Wait for PR build and comment
|
||||||
|
if: ${{ steps.auth.outputs.allowed == 'true' && env.IS_DRAFT != 'true' }}
|
||||||
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
const run = context.payload.workflow_run;
|
|
||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
const repo = context.repo.repo;
|
const repo = context.repo.repo;
|
||||||
|
const prNumber = Number(process.env.PR_NUMBER);
|
||||||
|
const headSha = process.env.HEAD_SHA;
|
||||||
|
const retentionDays = Number(process.env.RETENTION_DAYS || '7');
|
||||||
|
const marker = '<!-- codenomad-pr-artifacts -->';
|
||||||
|
|
||||||
const prs = run.pull_requests || [];
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
let prNumber = prs[0]?.number;
|
|
||||||
|
|
||||||
// `workflow_run` payload does not reliably include pull request numbers.
|
let matchedRun = null;
|
||||||
// Resolve PR number(s) by asking GitHub for PRs associated with the head SHA.
|
for (let attempt = 1; attempt <= 30; attempt += 1) {
|
||||||
if (!prNumber) {
|
const runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
|
||||||
const headSha = run.head_sha;
|
|
||||||
if (!headSha) {
|
|
||||||
core.info('No PR number and no head_sha found for this workflow_run; skipping.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const associated = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
commit_sha: headSha,
|
workflow_id: 'pr-build.yml',
|
||||||
|
event: 'pull_request',
|
||||||
|
per_page: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const open = (associated.data || []).find((p) => p.state === 'open');
|
const matchingRuns = runs
|
||||||
prNumber = open?.number;
|
.filter((run) => run.head_sha === headSha)
|
||||||
if (!prNumber) {
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
core.info(`No open PR found associated with commit ${headSha}; skipping.`);
|
|
||||||
return;
|
matchedRun = matchingRuns[0] || null;
|
||||||
|
if (matchedRun && matchedRun.status === 'completed') {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core.info(`Waiting for PR Build Validation run for ${headSha} (attempt ${attempt}/30)`);
|
||||||
|
await sleep(10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only comment when the PR build job actually ran (i.e. authorization passed).
|
if (!matchedRun) {
|
||||||
// Unauthorized PRs targeting non-dev will skip the `build` job.
|
core.setFailed(`Could not find PR Build Validation run for ${headSha}.`);
|
||||||
const jobs = await github.paginate(
|
return;
|
||||||
github.rest.actions.listJobsForWorkflowRun,
|
}
|
||||||
{ owner, repo, run_id: run.id, per_page: 100 }
|
|
||||||
);
|
if (matchedRun.status !== 'completed') {
|
||||||
const buildJob = jobs.find((j) => j.name === 'build');
|
core.setFailed(`PR Build Validation run ${matchedRun.id} did not complete in time.`);
|
||||||
if (!buildJob) {
|
|
||||||
core.info('No `build` job found on this run; skipping.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (buildJob.conclusion === 'skipped') {
|
|
||||||
core.info('`build` job was skipped; skipping comment.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List artifacts from the run. If none exist (e.g. build failed before packaging),
|
|
||||||
// still comment with the run link so testers can see logs.
|
|
||||||
const artifacts = await github.paginate(
|
const artifacts = await github.paginate(
|
||||||
github.rest.actions.listWorkflowRunArtifacts,
|
github.rest.actions.listWorkflowRunArtifacts,
|
||||||
{ owner, repo, run_id: run.id, per_page: 100 }
|
{ owner, repo, run_id: matchedRun.id, per_page: 100 }
|
||||||
);
|
);
|
||||||
const active = artifacts.filter((a) => !a.expired);
|
const active = artifacts.filter((artifact) => !artifact.expired);
|
||||||
|
|
||||||
const marker = '<!-- codenomad-pr-artifacts -->';
|
|
||||||
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${run.id}`;
|
|
||||||
const retentionDays = 7;
|
|
||||||
|
|
||||||
|
const runUrl = matchedRun.html_url;
|
||||||
const artifactsBlock = active.length
|
const artifactsBlock = active.length
|
||||||
? ['Artifacts:', ...active.map((a) => `- ${a.name}`)].join('\n')
|
? ['Artifacts:', ...active.map((artifact) => `- ${artifact.name}`)].join('\n')
|
||||||
: 'Artifacts: (none found on this run)';
|
: 'Artifacts: (none found on this run)';
|
||||||
|
|
||||||
const body = [
|
const body = [
|
||||||
@@ -95,26 +112,10 @@ jobs:
|
|||||||
artifactsBlock,
|
artifactsBlock,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const comments = await github.paginate(
|
const created = await github.rest.issues.createComment({
|
||||||
github.rest.issues.listComments,
|
owner,
|
||||||
{ owner, repo, issue_number: prNumber, per_page: 100 }
|
repo,
|
||||||
);
|
issue_number: prNumber,
|
||||||
const existing = comments.find((c) => (c.body || '').includes(marker));
|
body,
|
||||||
|
});
|
||||||
if (existing) {
|
core.info(`Created artifacts comment: ${created.data.html_url}`);
|
||||||
await github.rest.issues.updateComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
comment_id: existing.id,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
core.info(`Updated existing artifacts comment: ${existing.html_url}`);
|
|
||||||
} else {
|
|
||||||
const created = await github.rest.issues.createComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
core.info(`Created artifacts comment: ${created.data.html_url}`);
|
|
||||||
}
|
|
||||||
|
|||||||
3
.github/workflows/pr-build.yml
vendored
3
.github/workflows/pr-build.yml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
- opened
|
- opened
|
||||||
- synchronize
|
- synchronize
|
||||||
- reopened
|
- reopened
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -45,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
needs: authorize
|
needs: authorize
|
||||||
if: ${{ needs.authorize.outputs.allowed == 'true' }}
|
if: ${{ needs.authorize.outputs.allowed == 'true' && !github.event.pull_request.draft }}
|
||||||
uses: ./.github/workflows/build-and-upload.yml
|
uses: ./.github/workflows/build-and-upload.yml
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|||||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -10984,6 +10984,36 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/virtua": {
|
||||||
|
"version": "0.48.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.48.8.tgz",
|
||||||
|
"integrity": "sha512-jpsxOw5V4B6hg44JePRLo9DL0TV7N1lBEVtPjKpAJebXyhI2s9lfiXJESaLapNtr3vtiSk/pWHiLf7B2a6UcgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.14.0",
|
||||||
|
"react-dom": ">=16.14.0",
|
||||||
|
"solid-js": ">=1.0",
|
||||||
|
"svelte": ">=5.0",
|
||||||
|
"vue": ">=3.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"solid-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -12113,6 +12143,7 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
|
"virtua": "^0.48.8",
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.11"
|
"baseline-browser-mapping": "^2.9.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.2.25"
|
"@opencode-ai/plugin": "1.2.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,4 +46,4 @@
|
|||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
67
packages/tauri-app/Cargo.lock
generated
67
packages/tauri-app/Cargo.lock
generated
@@ -473,6 +473,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -1350,6 +1351,16 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gethostname"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||||
|
dependencies = [
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
@@ -1482,6 +1493,24 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "global-hotkey"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"keyboard-types",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
"x11rb",
|
||||||
|
"xkeysym",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
@@ -4055,6 +4084,21 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-global-shortcut"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405"
|
||||||
|
dependencies = [
|
||||||
|
"global-hotkey",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-notification"
|
name = "tauri-plugin-notification"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
@@ -5735,6 +5779,29 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x11rb"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||||
|
dependencies = [
|
||||||
|
"gethostname",
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"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]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ keepawake = "0.6"
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-global-shortcut = "2"
|
||||||
url = "2"
|
url = "2"
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"core:menu:default",
|
"core:menu:default",
|
||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
"opener:allow-default-urls",
|
"opener:allow-default-urls",
|
||||||
|
"opener:allow-open-url",
|
||||||
"notification:allow-is-permission-granted",
|
"notification:allow-is-permission-granted",
|
||||||
"notification:allow-request-permission",
|
"notification:allow-request-permission",
|
||||||
"notification:allow-notify",
|
"notification:allow-notify",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-show","core:webview:allow-set-webview-zoom"]}}
|
{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","opener:allow-open-url","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-show","core:webview:allow-set-webview-zoom"]}}
|
||||||
|
|||||||
@@ -2378,6 +2378,72 @@
|
|||||||
"const": "dialog:deny-save",
|
"const": "dialog:deny-save",
|
||||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:default",
|
||||||
|
"markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_registered command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:allow-is-registered",
|
||||||
|
"markdownDescription": "Enables the is_registered command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:allow-register",
|
||||||
|
"markdownDescription": "Enables the register command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_all command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:allow-register-all",
|
||||||
|
"markdownDescription": "Enables the register_all command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the unregister command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:allow-unregister",
|
||||||
|
"markdownDescription": "Enables the unregister command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the unregister_all command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:allow-unregister-all",
|
||||||
|
"markdownDescription": "Enables the unregister_all command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_registered command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:deny-is-registered",
|
||||||
|
"markdownDescription": "Denies the is_registered command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:deny-register",
|
||||||
|
"markdownDescription": "Denies the register command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_all command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:deny-register-all",
|
||||||
|
"markdownDescription": "Denies the register_all command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the unregister command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:deny-unregister",
|
||||||
|
"markdownDescription": "Denies the unregister command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the unregister_all command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "global-shortcut:deny-unregister-all",
|
||||||
|
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ use serde::Deserialize;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
||||||
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
||||||
use tauri::webview::Webview;
|
use tauri::webview::Webview;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, Wry};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WindowEvent, Wry};
|
||||||
|
use tauri_plugin_global_shortcut::{
|
||||||
|
Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState,
|
||||||
|
};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@@ -25,6 +29,10 @@ use std::os::windows::ffi::OsStrExt;
|
|||||||
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||||
|
|
||||||
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
const DEFAULT_ZOOM_LEVEL: f64 = 1.0;
|
||||||
|
const ZOOM_STEP: f64 = 0.2;
|
||||||
|
const MIN_ZOOM_LEVEL: f64 = 0.2;
|
||||||
|
const MAX_ZOOM_LEVEL: f64 = 5.0;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
||||||
@@ -32,6 +40,7 @@ const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub manager: CliProcessManager,
|
pub manager: CliProcessManager,
|
||||||
pub wake_lock: Mutex<Option<KeepAwake>>,
|
pub wake_lock: Mutex<Option<KeepAwake>>,
|
||||||
|
pub zoom_level: Mutex<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
@@ -157,6 +166,83 @@ fn emit_folder_drop_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clamp_zoom_level(value: f64) -> f64 {
|
||||||
|
value.clamp(MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_main_window_zoom(app_handle: &AppHandle, next_zoom: f64) {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
let normalized = clamp_zoom_level(next_zoom);
|
||||||
|
if window.set_zoom(normalized).is_ok() {
|
||||||
|
if let Ok(mut zoom_level) = app_handle.state::<AppState>().zoom_level.lock() {
|
||||||
|
*zoom_level = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_main_window(app_handle: &AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
let _ = window.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn force_reload_main_window(app_handle: &AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
if let Ok(mut url) = window.url() {
|
||||||
|
if should_allow_internal(&url) {
|
||||||
|
let reload_token = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let existing_pairs: Vec<(String, String)> = url
|
||||||
|
.query_pairs()
|
||||||
|
.into_owned()
|
||||||
|
.filter(|(key, _)| key != "__codenomad_force_reload")
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pairs = url.query_pairs_mut();
|
||||||
|
pairs.clear();
|
||||||
|
for (key, value) in existing_pairs {
|
||||||
|
pairs.append_pair(&key, &value);
|
||||||
|
}
|
||||||
|
pairs.append_pair("__codenomad_force_reload", &reload_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = window.navigate(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = window.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_fullscreen_window(app_handle: &AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
let next_fullscreen = !window.is_fullscreen().unwrap_or(false);
|
||||||
|
let _ = window.set_fullscreen(next_fullscreen);
|
||||||
|
if cfg!(not(target_os = "macos")) {
|
||||||
|
if next_fullscreen {
|
||||||
|
let _ = window.hide_menu();
|
||||||
|
} else {
|
||||||
|
let _ = window.show_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fullscreen_shortcut() -> Option<Shortcut> {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Shortcut::new(None, ShortcutCode::F11))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn set_windows_app_user_model_id() {
|
fn set_windows_app_user_model_id() {
|
||||||
let app_id: Vec<u16> = OsStr::new(WINDOWS_APP_USER_MODEL_ID)
|
let app_id: Vec<u16> = OsStr::new(WINDOWS_APP_USER_MODEL_ID)
|
||||||
@@ -181,15 +267,48 @@ fn main() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(
|
||||||
|
tauri_plugin_global_shortcut::Builder::new()
|
||||||
|
.with_handler(|app, shortcut, event| {
|
||||||
|
if event.state() != ShortcutState::Pressed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullscreen_shortcut().as_ref() == Some(shortcut) {
|
||||||
|
toggle_fullscreen_window(app);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(navigation_guard)
|
.plugin(navigation_guard)
|
||||||
.manage(AppState {
|
.manage(AppState {
|
||||||
manager: CliProcessManager::new(),
|
manager: CliProcessManager::new(),
|
||||||
wake_lock: Mutex::new(None),
|
wake_lock: Mutex::new(None),
|
||||||
|
zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL),
|
||||||
})
|
})
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
set_windows_app_user_model_id();
|
set_windows_app_user_model_id();
|
||||||
build_menu(&app.handle())?;
|
build_menu(&app.handle())?;
|
||||||
|
if let Some(shortcut) = fullscreen_shortcut() {
|
||||||
|
let shortcut_manager = app.handle().global_shortcut();
|
||||||
|
let _ = shortcut_manager.register(shortcut.clone());
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let WindowEvent::Focused(focused) = event {
|
||||||
|
let shortcut_manager = app_handle.global_shortcut();
|
||||||
|
if *focused {
|
||||||
|
let _ = shortcut_manager.register(shortcut.clone());
|
||||||
|
} else {
|
||||||
|
let _ = shortcut_manager.unregister(shortcut.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let dev_mode = is_dev_mode();
|
let dev_mode = is_dev_mode();
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
let manager = app.state::<AppState>().manager.clone();
|
let manager = app.state::<AppState>().manager.clone();
|
||||||
@@ -214,36 +333,42 @@ fn main() {
|
|||||||
let _ = window.emit("menu:newInstance", ());
|
let _ = window.emit("menu:newInstance", ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"close" => {
|
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
|
||||||
let _ = window.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"quit" => {
|
"quit" => {
|
||||||
app_handle.exit(0);
|
app_handle.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// View menu
|
// View menu
|
||||||
"reload" => {
|
"reload" => {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
reload_main_window(app_handle);
|
||||||
let _ = window.eval("window.location.reload()");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"force_reload" => {
|
"force_reload" => {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
force_reload_main_window(app_handle);
|
||||||
let _ = window.eval("window.location.reload(true)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"toggle_devtools" => {
|
"toggle_devtools" => {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
window.open_devtools();
|
if window.is_devtools_open() {
|
||||||
|
window.close_devtools();
|
||||||
|
} else {
|
||||||
|
window.open_devtools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"reset_zoom" => {
|
||||||
|
set_main_window_zoom(app_handle, DEFAULT_ZOOM_LEVEL);
|
||||||
|
}
|
||||||
|
"zoom_in" => {
|
||||||
|
if let Ok(zoom_level) = app_handle.state::<AppState>().zoom_level.lock() {
|
||||||
|
set_main_window_zoom(app_handle, *zoom_level + ZOOM_STEP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"zoom_out" => {
|
||||||
|
if let Ok(zoom_level) = app_handle.state::<AppState>().zoom_level.lock() {
|
||||||
|
set_main_window_zoom(app_handle, *zoom_level - ZOOM_STEP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"toggle_fullscreen" => {
|
"toggle_fullscreen" => {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
toggle_fullscreen_window(app_handle);
|
||||||
let _ = window.set_fullscreen(!window.is_fullscreen().unwrap_or(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window menu
|
// Window menu
|
||||||
@@ -257,6 +382,11 @@ fn main() {
|
|||||||
let _ = window.maximize();
|
let _ = window.maximize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"close_window" => {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
let _ = window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// App menu (macOS)
|
// App menu (macOS)
|
||||||
"about" => {
|
"about" => {
|
||||||
@@ -344,6 +474,7 @@ fn main() {
|
|||||||
|
|
||||||
fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||||
let is_mac = cfg!(target_os = "macos");
|
let is_mac = cfg!(target_os = "macos");
|
||||||
|
let is_linux = cfg!(target_os = "linux");
|
||||||
|
|
||||||
// Create submenus
|
// Create submenus
|
||||||
let mut submenus = Vec::new();
|
let mut submenus = Vec::new();
|
||||||
@@ -371,16 +502,74 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
|||||||
Some("CmdOrCtrl+N"),
|
Some("CmdOrCtrl+N"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let file_menu = SubmenuBuilder::new(app, "File")
|
let file_menu = if is_mac {
|
||||||
.item(&new_instance_item)
|
SubmenuBuilder::new(app, "File")
|
||||||
.separator()
|
.item(&new_instance_item)
|
||||||
.text(
|
.separator()
|
||||||
if is_mac { "close" } else { "quit" },
|
.close_window()
|
||||||
if is_mac { "Close" } else { "Quit" },
|
.build()?
|
||||||
)
|
} else {
|
||||||
.build()?;
|
SubmenuBuilder::new(app, "File")
|
||||||
|
.item(&new_instance_item)
|
||||||
|
.separator()
|
||||||
|
.text("quit", "Quit")
|
||||||
|
.build()?
|
||||||
|
};
|
||||||
submenus.push(file_menu);
|
submenus.push(file_menu);
|
||||||
|
|
||||||
|
let reload_item = MenuItem::with_id(app, "reload", "Reload", true, Some("CmdOrCtrl+R"))?;
|
||||||
|
let force_reload_item = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"force_reload",
|
||||||
|
"Force Reload",
|
||||||
|
true,
|
||||||
|
Some("CmdOrCtrl+Shift+R"),
|
||||||
|
)?;
|
||||||
|
let toggle_devtools_item = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"toggle_devtools",
|
||||||
|
"Toggle Developer Tools",
|
||||||
|
true,
|
||||||
|
Some("Alt+CmdOrCtrl+I"),
|
||||||
|
)?;
|
||||||
|
let reset_zoom_item =
|
||||||
|
MenuItem::with_id(app, "reset_zoom", "Actual Size", true, Some("CmdOrCtrl+0"))?;
|
||||||
|
let zoom_in_item = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"zoom_in",
|
||||||
|
if is_mac { "Zoom In" } else { "Zoom In\tCtrl++" },
|
||||||
|
true,
|
||||||
|
None::<&str>,
|
||||||
|
)?;
|
||||||
|
let zoom_out_item = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"zoom_out",
|
||||||
|
if is_mac {
|
||||||
|
"Zoom Out"
|
||||||
|
} else {
|
||||||
|
"Zoom Out\tCtrl+-"
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
None::<&str>,
|
||||||
|
)?;
|
||||||
|
let toggle_fullscreen_item = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"toggle_fullscreen",
|
||||||
|
if is_mac {
|
||||||
|
"Toggle Full Screen"
|
||||||
|
} else {
|
||||||
|
"Toggle Full Screen\tF11"
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
if is_mac {
|
||||||
|
Some("Ctrl+Cmd+F")
|
||||||
|
} else {
|
||||||
|
None::<&str>
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let close_window_item =
|
||||||
|
MenuItem::with_id(app, "close_window", "Close", true, Some("CmdOrCtrl+W"))?;
|
||||||
|
|
||||||
// Edit menu with predefined items for standard functionality
|
// Edit menu with predefined items for standard functionality
|
||||||
let edit_menu = SubmenuBuilder::new(app, "Edit")
|
let edit_menu = SubmenuBuilder::new(app, "Edit")
|
||||||
.undo()
|
.undo()
|
||||||
@@ -396,20 +585,39 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
|||||||
|
|
||||||
// View menu
|
// View menu
|
||||||
let view_menu = SubmenuBuilder::new(app, "View")
|
let view_menu = SubmenuBuilder::new(app, "View")
|
||||||
.text("reload", "Reload")
|
.item(&reload_item)
|
||||||
.text("force_reload", "Force Reload")
|
.item(&force_reload_item)
|
||||||
.text("toggle_devtools", "Toggle Developer Tools")
|
.item(&toggle_devtools_item)
|
||||||
.separator()
|
.separator()
|
||||||
|
.item(&reset_zoom_item)
|
||||||
|
.item(&zoom_in_item)
|
||||||
|
.item(&zoom_out_item)
|
||||||
.separator()
|
.separator()
|
||||||
.text("toggle_fullscreen", "Toggle Full Screen")
|
.item(&toggle_fullscreen_item)
|
||||||
.build()?;
|
.build()?;
|
||||||
submenus.push(view_menu);
|
submenus.push(view_menu);
|
||||||
|
|
||||||
// Window menu
|
// Window menu
|
||||||
let window_menu = SubmenuBuilder::new(app, "Window")
|
let window_menu = if is_linux {
|
||||||
.text("minimize", "Minimize")
|
SubmenuBuilder::new(app, "Window")
|
||||||
.text("zoom", "Zoom")
|
.text("minimize", "Minimize")
|
||||||
.build()?;
|
.text("zoom", "Zoom")
|
||||||
|
.separator()
|
||||||
|
.item(&close_window_item)
|
||||||
|
.build()?
|
||||||
|
} else if is_mac {
|
||||||
|
SubmenuBuilder::new(app, "Window")
|
||||||
|
.minimize()
|
||||||
|
.maximize()
|
||||||
|
.build()?
|
||||||
|
} else {
|
||||||
|
SubmenuBuilder::new(app, "Window")
|
||||||
|
.minimize()
|
||||||
|
.maximize()
|
||||||
|
.separator()
|
||||||
|
.close_window()
|
||||||
|
.build()?
|
||||||
|
};
|
||||||
submenus.push(window_menu);
|
submenus.push(window_menu);
|
||||||
|
|
||||||
// Build the main menu with all submenus
|
// Build the main menu with all submenus
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
|
"virtua": "^0.48.8",
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -44,4 +45,4 @@
|
|||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vite-plugin-solid": "^2.10.0"
|
"vite-plugin-solid": "^2.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,8 @@ import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
|||||||
import InstanceShell from "./components/instance/instance-shell2"
|
import InstanceShell from "./components/instance/instance-shell2"
|
||||||
import { SettingsScreen } from "./components/settings-screen"
|
import { SettingsScreen } from "./components/settings-screen"
|
||||||
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
|
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
|
||||||
import { initMarkdown } from "./lib/markdown"
|
|
||||||
import { initGithubStars } from "./stores/github-stars"
|
import { initGithubStars } from "./stores/github-stars"
|
||||||
|
|
||||||
import { useTheme } from "./lib/theme"
|
|
||||||
import { useCommands } from "./lib/hooks/use-commands"
|
import { useCommands } from "./lib/hooks/use-commands"
|
||||||
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
||||||
import { getLogger } from "./lib/logger"
|
import { getLogger } from "./lib/logger"
|
||||||
@@ -59,7 +57,6 @@ import { openSettings } from "./stores/settings-screen"
|
|||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
const { isDark } = useTheme()
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const {
|
const {
|
||||||
preferences,
|
preferences,
|
||||||
@@ -183,10 +180,6 @@ const App: Component = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
initReleaseNotifications()
|
initReleaseNotifications()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createSignal, onMount, Show, createEffect } from "solid-js"
|
import { createSignal, onMount, Show, createEffect } from "solid-js"
|
||||||
import type { Highlighter } from "shiki/bundle/full"
|
import type { Highlighter } from "shiki/bundle/full"
|
||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
import { getSharedHighlighter, escapeHtml } from "../lib/markdown"
|
import { getSharedHighlighter } from "../lib/markdown"
|
||||||
|
import { escapeHtml } from "../lib/text-render-utils"
|
||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createMemo, Show, createEffect, onCleanup } from "solid-js"
|
import { createMemo, Show, createEffect, onCleanup } from "solid-js"
|
||||||
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
||||||
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
import { disableCache } from "@git-diff-view/core"
|
import { disableCache } from "@git-diff-view/core"
|
||||||
import type { DiffHighlighterLang } from "@git-diff-view/core"
|
import type { DiffHighlighterLang } from "@git-diff-view/core"
|
||||||
import { ErrorBoundary } from "solid-js"
|
import { ErrorBoundary } from "solid-js"
|
||||||
import { getLanguageFromPath } from "../lib/markdown"
|
import { getLanguageFromPath } from "../lib/text-render-utils"
|
||||||
import { normalizeDiffText } from "../lib/diff-utils"
|
import { normalizeDiffText } from "../lib/diff-utils"
|
||||||
import { setCacheEntry } from "../lib/global-cache"
|
import { setCacheEntry } from "../lib/global-cache"
|
||||||
import type { CacheEntryParams } from "../lib/global-cache"
|
import type { CacheEntryParams } from "../lib/global-cache"
|
||||||
@@ -134,4 +135,4 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import { formatCompactCount } from "../lib/formatters"
|
|||||||
import { useI18n, type Locale } from "../lib/i18n"
|
import { useI18n, type Locale } from "../lib/i18n"
|
||||||
import { showAlertDialog } from "../stores/alerts"
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { openSettings, settingsOpen } from "../stores/settings-screen"
|
import { openSettings, settingsOpen } from "../stores/settings-screen"
|
||||||
|
import { openExternalUrl } from "../lib/external-url"
|
||||||
|
|
||||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||||
|
const GITHUB_URL = "https://github.com/NeuralNomadsAI/CodeNomad"
|
||||||
|
const DISCORD_URL = "https://discord.com/channels/1391832426048651334/1458412028325793887/1464701235683917945"
|
||||||
|
|
||||||
|
|
||||||
interface FolderSelectionViewProps {
|
interface FolderSelectionViewProps {
|
||||||
@@ -42,6 +45,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
{ value: "ru", label: "Русский" },
|
{ value: "ru", label: "Русский" },
|
||||||
{ value: "ja", label: "日本語" },
|
{ value: "ja", label: "日本語" },
|
||||||
{ value: "zh-Hans", label: "简体中文" },
|
{ value: "zh-Hans", label: "简体中文" },
|
||||||
|
{ value: "he", label: "עברית" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const selectedLanguageOption = () => languageOptions.find((opt) => opt.value === locale()) ?? languageOptions[0]
|
const selectedLanguageOption = () => languageOptions.find((opt) => opt.value === locale()) ?? languageOptions[0]
|
||||||
@@ -232,11 +236,6 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
props.onSelectFolder(path, selectedBinary())
|
props.onSelectFolder(path, selectedBinary())
|
||||||
}
|
}
|
||||||
|
|
||||||
const openExternalLink = (url: string) => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
window.open(url, "_blank", "noopener,noreferrer")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleBrowse() {
|
async function handleBrowse() {
|
||||||
if (isLoading()) return
|
if (isLoading()) return
|
||||||
setFocusMode("new")
|
setFocusMode("new")
|
||||||
@@ -343,7 +342,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
class="w-full max-w-5xl h-full px-4 sm:px-8 pb-2 flex flex-col overflow-hidden"
|
class="w-full max-w-5xl h-full px-4 sm:px-8 pb-2 flex flex-col overflow-hidden"
|
||||||
aria-busy={isLoading() ? "true" : "false"}
|
aria-busy={isLoading() ? "true" : "false"}
|
||||||
>
|
>
|
||||||
<div class="absolute top-4 left-6">
|
<div class="absolute top-4" style="inset-inline-start: 1.5rem;">
|
||||||
<Select<LanguageOption>
|
<Select<LanguageOption>
|
||||||
value={selectedLanguageOption()}
|
value={selectedLanguageOption()}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -387,7 +386,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</Select.Portal>
|
</Select.Portal>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-4 right-6 flex items-center gap-2">
|
<div class="absolute top-4 flex items-center gap-2" style="inset-inline-end: 1.5rem;">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||||
@@ -425,7 +424,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
<h1 class="mb-2 text-3xl font-semibold text-primary">CodeNomad</h1>
|
<h1 class="mb-2 text-3xl font-semibold text-primary">CodeNomad</h1>
|
||||||
<div class="mt-3 flex justify-center gap-2">
|
<div class="mt-3 flex justify-center gap-2">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/NeuralNomadsAI/CodeNomad"
|
href={GITHUB_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||||
@@ -433,13 +432,13 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
title={t("folderSelection.links.github")}
|
title={t("folderSelection.links.github")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
openExternalLink("https://github.com/NeuralNomadsAI/CodeNomad")
|
void openExternalUrl(GITHUB_URL, "folder-selection")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GitHubMarkIcon class="w-4 h-4" />
|
<GitHubMarkIcon class="w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/NeuralNomadsAI/CodeNomad"
|
href={GITHUB_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
|
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
|
||||||
@@ -447,7 +446,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
title={t("folderSelection.links.githubStars")}
|
title={t("folderSelection.links.githubStars")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
openExternalLink("https://github.com/NeuralNomadsAI/CodeNomad")
|
void openExternalUrl(GITHUB_URL, "folder-selection")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Star class="w-4 h-4" />
|
<Star class="w-4 h-4" />
|
||||||
@@ -456,7 +455,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://discord.com/channels/1391832426048651334/1458412028325793887/1464701235683917945"
|
href={DISCORD_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||||
@@ -464,9 +463,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
title={t("folderSelection.links.discord")}
|
title={t("folderSelection.links.discord")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
openExternalLink(
|
void openExternalUrl(DISCORD_URL, "folder-selection")
|
||||||
"https://discord.com/channels/1391832426048651334/1458412028325793887/1464701235683917945",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DiscordSymbolIcon class="w-4 h-4" />
|
<DiscordSymbolIcon class="w-4 h-4" />
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
<div class="panel-body space-y-3">
|
<div class="panel-body space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">{t("instanceInfo.labels.folder")}</div>
|
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">{t("instanceInfo.labels.folder")}</div>
|
||||||
<div class="text-xs text-primary font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
<div dir="ltr" class="text-xs text-primary font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
||||||
{currentInstance().folder}
|
{currentInstance().folder}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
|
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
|
||||||
{t("instanceInfo.labels.project")}
|
{t("instanceInfo.labels.project")}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs font-mono px-2 py-1.5 rounded border truncate bg-surface-secondary border-base text-primary">
|
<div dir="ltr" class="text-xs font-mono px-2 py-1.5 rounded border truncate bg-surface-secondary border-base text-primary">
|
||||||
{project().id}
|
{project().id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +137,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
|
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
|
||||||
{t("instanceInfo.labels.binaryPath")}
|
{t("instanceInfo.labels.binaryPath")}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
|
<div dir="ltr" class="text-xs font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
|
||||||
{currentInstance().binaryPath}
|
{currentInstance().binaryPath}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +151,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<For each={environmentEntries()}>
|
<For each={environmentEntries()}>
|
||||||
{([key, value]) => (
|
{([key, value]) => (
|
||||||
<div class="flex items-center gap-2 px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
<div dir="ltr" class="flex items-center gap-2 px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
||||||
<span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
|
<span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
|
||||||
{key}
|
{key}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-medium text-primary whitespace-normal break-words transition-colors"
|
class="text-sm font-medium text-primary whitespace-normal break-words transition-colors"
|
||||||
|
dir="auto"
|
||||||
classList={{
|
classList={{
|
||||||
"text-accent": isFocused(),
|
"text-accent": isFocused(),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ interface InstanceShellProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||||
const { t } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
const isRTL = () => locale() === "he"
|
||||||
|
|
||||||
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
|
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
|
||||||
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(
|
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(
|
||||||
@@ -371,7 +372,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: `${sessionSidebarWidth()}px`,
|
width: `${sessionSidebarWidth()}px`,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderRight: "1px solid var(--border-base)",
|
borderInlineEnd: "1px solid var(--border-base)",
|
||||||
backgroundColor: "var(--surface-secondary)",
|
backgroundColor: "var(--surface-secondary)",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
@@ -413,7 +414,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const modalProps = container ? { container: container as Element } : undefined
|
const modalProps = container ? { container: container as Element } : undefined
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
anchor="left"
|
anchor={isRTL() ? "right" : "left"}
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
open={leftOpen()}
|
open={leftOpen()}
|
||||||
onClose={closeLeftDrawer}
|
onClose={closeLeftDrawer}
|
||||||
@@ -422,7 +423,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
|
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
borderRight: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
|
borderInlineEnd: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
|
||||||
backgroundColor: "var(--surface-secondary)",
|
backgroundColor: "var(--surface-secondary)",
|
||||||
backgroundImage: "none",
|
backgroundImage: "none",
|
||||||
color: "var(--text-primary)",
|
color: "var(--text-primary)",
|
||||||
@@ -480,7 +481,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
width: `${rightDrawerWidth()}px`,
|
width: `${rightDrawerWidth()}px`,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderLeft: "1px solid var(--border-base)",
|
borderInlineStart: "1px solid var(--border-base)",
|
||||||
backgroundColor: "var(--surface-secondary)",
|
backgroundColor: "var(--surface-secondary)",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
@@ -523,7 +524,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const modalProps = container ? { container: container as Element } : undefined
|
const modalProps = container ? { container: container as Element } : undefined
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
anchor="right"
|
anchor={isRTL() ? "left" : "right"}
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
open={rightOpen()}
|
open={rightOpen()}
|
||||||
onClose={closeRightDrawer}
|
onClose={closeRightDrawer}
|
||||||
@@ -532,7 +533,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
|
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
borderLeft: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
|
borderInlineStart: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
|
||||||
backgroundColor: "var(--surface-secondary)",
|
backgroundColor: "var(--surface-secondary)",
|
||||||
backgroundImage: "none",
|
backgroundImage: "none",
|
||||||
color: "var(--text-primary)",
|
color: "var(--text-primary)",
|
||||||
@@ -742,7 +743,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<Kbd shortcut="cmd+shift+p" />
|
<Kbd shortcut="cmd+shift+p" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-3">
|
<div class="ms-auto flex items-center gap-3">
|
||||||
<div class="connection-status-meta flex items-center gap-3">
|
<div class="connection-status-meta flex items-center gap-3">
|
||||||
<Show when={connectionStatus() === "connected"}>
|
<Show when={connectionStatus() === "connected"}>
|
||||||
<span class="status-indicator connected">
|
<span class="status-indicator connected">
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Show,
|
Show,
|
||||||
|
Suspense,
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createSignal,
|
createSignal,
|
||||||
|
lazy,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
type Accessor,
|
type Accessor,
|
||||||
type Component,
|
type Component,
|
||||||
@@ -20,11 +22,6 @@ import type { Session } from "../../../../types/session"
|
|||||||
import type { DrawerViewState } from "../types"
|
import type { DrawerViewState } from "../types"
|
||||||
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } from "./types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } from "./types"
|
||||||
|
|
||||||
import ChangesTab from "./tabs/ChangesTab"
|
|
||||||
import FilesTab from "./tabs/FilesTab"
|
|
||||||
import GitChangesTab from "./tabs/GitChangesTab"
|
|
||||||
import StatusTab from "./tabs/StatusTab"
|
|
||||||
|
|
||||||
import { getDefaultWorktreeSlug, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "../../../../stores/worktrees"
|
import { getDefaultWorktreeSlug, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "../../../../stores/worktrees"
|
||||||
import { requestData } from "../../../../lib/opencode-api"
|
import { requestData } from "../../../../lib/opencode-api"
|
||||||
import { buildUnifiedDiffFromSdkPatch, tryReverseApplyUnifiedDiff } from "../../../../lib/unified-diff-reverse"
|
import { buildUnifiedDiffFromSdkPatch, tryReverseApplyUnifiedDiff } from "../../../../lib/unified-diff-reverse"
|
||||||
@@ -49,6 +46,15 @@ import {
|
|||||||
readStoredRightPanelTab,
|
readStoredRightPanelTab,
|
||||||
} from "../storage"
|
} from "../storage"
|
||||||
|
|
||||||
|
const LazyChangesTab = lazy(() => import("./tabs/ChangesTab"))
|
||||||
|
const LazyGitChangesTab = lazy(() => import("./tabs/GitChangesTab"))
|
||||||
|
const LazyFilesTab = lazy(() => import("./tabs/FilesTab"))
|
||||||
|
const LazyStatusTab = lazy(() => import("./tabs/StatusTab"))
|
||||||
|
|
||||||
|
function RightPanelTabFallback() {
|
||||||
|
return <div class="flex-1 min-h-0" />
|
||||||
|
}
|
||||||
|
|
||||||
interface RightPanelProps {
|
interface RightPanelProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
|
|
||||||
@@ -243,7 +249,8 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
const mode = activeSplitResize()
|
const mode = activeSplitResize()
|
||||||
if (!mode) return
|
if (!mode) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const delta = event.clientX - splitResizeStartX()
|
const isRtl = typeof document !== "undefined" && document.documentElement.dir === "rtl"
|
||||||
|
const delta = (event.clientX - splitResizeStartX()) * (isRtl ? -1 : 1)
|
||||||
const next = clampSplitWidth(splitResizeStartWidth() + delta)
|
const next = clampSplitWidth(splitResizeStartWidth() + delta)
|
||||||
if (mode === "changes") setChangesSplitWidth(next)
|
if (mode === "changes") setChangesSplitWidth(next)
|
||||||
else if (mode === "git-changes") setGitChangesSplitWidth(next)
|
else if (mode === "git-changes") setGitChangesSplitWidth(next)
|
||||||
@@ -266,7 +273,8 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
const touch = event.touches[0]
|
const touch = event.touches[0]
|
||||||
if (!touch) return
|
if (!touch) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const delta = touch.clientX - splitResizeStartX()
|
const isRtl = typeof document !== "undefined" && document.documentElement.dir === "rtl"
|
||||||
|
const delta = (touch.clientX - splitResizeStartX()) * (isRtl ? -1 : 1)
|
||||||
const next = clampSplitWidth(splitResizeStartWidth() + delta)
|
const next = clampSplitWidth(splitResizeStartWidth() + delta)
|
||||||
if (mode === "changes") setChangesSplitWidth(next)
|
if (mode === "changes") setChangesSplitWidth(next)
|
||||||
else if (mode === "git-changes") setGitChangesSplitWidth(next)
|
else if (mode === "git-changes") setGitChangesSplitWidth(next)
|
||||||
@@ -565,6 +573,13 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
void loadBrowserEntries(browserPath())
|
void loadBrowserEntries(browserPath())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (rightPanelTab() === "files") return
|
||||||
|
setBrowserSelectedContent(null)
|
||||||
|
setBrowserSelectedLoading(false)
|
||||||
|
setBrowserSelectedError(null)
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (rightPanelTab() !== "git-changes") return
|
if (rightPanelTab() !== "git-changes") return
|
||||||
if (gitStatusLoading()) return
|
if (gitStatusLoading()) return
|
||||||
@@ -572,6 +587,14 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
void loadGitStatus()
|
void loadGitStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (rightPanelTab() === "git-changes") return
|
||||||
|
setGitSelectedBefore(null)
|
||||||
|
setGitSelectedAfter(null)
|
||||||
|
setGitSelectedLoading(false)
|
||||||
|
setGitSelectedError(null)
|
||||||
|
})
|
||||||
|
|
||||||
const handleSelectChangesFile = (file: string, closeList: boolean) => {
|
const handleSelectChangesFile = (file: string, closeList: boolean) => {
|
||||||
setSelectedFile(file)
|
setSelectedFile(file)
|
||||||
if (closeList) {
|
if (closeList) {
|
||||||
@@ -738,101 +761,109 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<Show when={rightPanelTab() === "changes"}>
|
<Show when={rightPanelTab() === "changes"}>
|
||||||
<ChangesTab
|
<Suspense fallback={<RightPanelTabFallback />}>
|
||||||
t={props.t}
|
<LazyChangesTab
|
||||||
instanceId={props.instanceId}
|
t={props.t}
|
||||||
activeSessionId={props.activeSessionId}
|
instanceId={props.instanceId}
|
||||||
activeSessionDiffs={props.activeSessionDiffs}
|
activeSessionId={props.activeSessionId}
|
||||||
selectedFile={selectedFile}
|
activeSessionDiffs={props.activeSessionDiffs}
|
||||||
onSelectFile={handleSelectChangesFile}
|
selectedFile={selectedFile}
|
||||||
diffViewMode={diffViewMode}
|
onSelectFile={handleSelectChangesFile}
|
||||||
diffContextMode={diffContextMode}
|
diffViewMode={diffViewMode}
|
||||||
diffWordWrapMode={diffWordWrapMode}
|
diffContextMode={diffContextMode}
|
||||||
onViewModeChange={setDiffViewMode}
|
diffWordWrapMode={diffWordWrapMode}
|
||||||
onContextModeChange={setDiffContextMode}
|
onViewModeChange={setDiffViewMode}
|
||||||
onWordWrapModeChange={setDiffWordWrapMode}
|
onContextModeChange={setDiffContextMode}
|
||||||
listOpen={changesListOpen}
|
onWordWrapModeChange={setDiffWordWrapMode}
|
||||||
onToggleList={toggleChangesList}
|
listOpen={changesListOpen}
|
||||||
splitWidth={changesSplitWidth}
|
onToggleList={toggleChangesList}
|
||||||
onResizeMouseDown={handleSplitResizeMouseDown("changes")}
|
splitWidth={changesSplitWidth}
|
||||||
onResizeTouchStart={handleSplitResizeTouchStart("changes")}
|
onResizeMouseDown={handleSplitResizeMouseDown("changes")}
|
||||||
isPhoneLayout={props.isPhoneLayout}
|
onResizeTouchStart={handleSplitResizeTouchStart("changes")}
|
||||||
/>
|
isPhoneLayout={props.isPhoneLayout}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={rightPanelTab() === "git-changes"}>
|
<Show when={rightPanelTab() === "git-changes"}>
|
||||||
<GitChangesTab
|
<Suspense fallback={<RightPanelTabFallback />}>
|
||||||
t={props.t}
|
<LazyGitChangesTab
|
||||||
activeSessionId={props.activeSessionId}
|
t={props.t}
|
||||||
entries={gitStatusEntries}
|
activeSessionId={props.activeSessionId}
|
||||||
statusLoading={gitStatusLoading}
|
entries={gitStatusEntries}
|
||||||
statusError={gitStatusError}
|
statusLoading={gitStatusLoading}
|
||||||
selectedPath={gitSelectedPath}
|
statusError={gitStatusError}
|
||||||
selectedLoading={gitSelectedLoading}
|
selectedPath={gitSelectedPath}
|
||||||
selectedError={gitSelectedError}
|
selectedLoading={gitSelectedLoading}
|
||||||
selectedBefore={gitSelectedBefore}
|
selectedError={gitSelectedError}
|
||||||
selectedAfter={gitSelectedAfter}
|
selectedBefore={gitSelectedBefore}
|
||||||
mostChangedPath={gitMostChangedPath}
|
selectedAfter={gitSelectedAfter}
|
||||||
scopeKey={gitScopeKey}
|
mostChangedPath={gitMostChangedPath}
|
||||||
diffViewMode={diffViewMode}
|
scopeKey={gitScopeKey}
|
||||||
diffContextMode={diffContextMode}
|
diffViewMode={diffViewMode}
|
||||||
diffWordWrapMode={diffWordWrapMode}
|
diffContextMode={diffContextMode}
|
||||||
onViewModeChange={setDiffViewMode}
|
diffWordWrapMode={diffWordWrapMode}
|
||||||
onContextModeChange={setDiffContextMode}
|
onViewModeChange={setDiffViewMode}
|
||||||
onWordWrapModeChange={setDiffWordWrapMode}
|
onContextModeChange={setDiffContextMode}
|
||||||
onOpenFile={(path) => void openGitFile(path)}
|
onWordWrapModeChange={setDiffWordWrapMode}
|
||||||
onRefresh={() => void refreshGitStatus()}
|
onOpenFile={(path: string) => void openGitFile(path)}
|
||||||
listOpen={gitChangesListOpen}
|
onRefresh={() => void refreshGitStatus()}
|
||||||
onToggleList={toggleGitList}
|
listOpen={gitChangesListOpen}
|
||||||
splitWidth={gitChangesSplitWidth}
|
onToggleList={toggleGitList}
|
||||||
onResizeMouseDown={handleSplitResizeMouseDown("git-changes")}
|
splitWidth={gitChangesSplitWidth}
|
||||||
onResizeTouchStart={handleSplitResizeTouchStart("git-changes")}
|
onResizeMouseDown={handleSplitResizeMouseDown("git-changes")}
|
||||||
isPhoneLayout={props.isPhoneLayout}
|
onResizeTouchStart={handleSplitResizeTouchStart("git-changes")}
|
||||||
/>
|
isPhoneLayout={props.isPhoneLayout}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={rightPanelTab() === "files"}>
|
<Show when={rightPanelTab() === "files"}>
|
||||||
<FilesTab
|
<Suspense fallback={<RightPanelTabFallback />}>
|
||||||
t={props.t}
|
<LazyFilesTab
|
||||||
browserPath={browserPath}
|
t={props.t}
|
||||||
browserEntries={browserEntries}
|
browserPath={browserPath}
|
||||||
browserLoading={browserLoading}
|
browserEntries={browserEntries}
|
||||||
browserError={browserError}
|
browserLoading={browserLoading}
|
||||||
browserSelectedPath={browserSelectedPath}
|
browserError={browserError}
|
||||||
browserSelectedContent={browserSelectedContent}
|
browserSelectedPath={browserSelectedPath}
|
||||||
browserSelectedLoading={browserSelectedLoading}
|
browserSelectedContent={browserSelectedContent}
|
||||||
browserSelectedError={browserSelectedError}
|
browserSelectedLoading={browserSelectedLoading}
|
||||||
parentPath={browserParentPath}
|
browserSelectedError={browserSelectedError}
|
||||||
scopeKey={browserScopeKey}
|
parentPath={browserParentPath}
|
||||||
onLoadEntries={(path) => void loadBrowserEntries(path)}
|
scopeKey={browserScopeKey}
|
||||||
onOpenFile={(path) => void openBrowserFile(path)}
|
onLoadEntries={(path: string) => void loadBrowserEntries(path)}
|
||||||
onRefresh={() => void refreshFilesTab()}
|
onOpenFile={(path: string) => void openBrowserFile(path)}
|
||||||
listOpen={filesListOpen}
|
onRefresh={() => void refreshFilesTab()}
|
||||||
onToggleList={toggleFilesList}
|
listOpen={filesListOpen}
|
||||||
splitWidth={filesSplitWidth}
|
onToggleList={toggleFilesList}
|
||||||
onResizeMouseDown={handleSplitResizeMouseDown("files")}
|
splitWidth={filesSplitWidth}
|
||||||
onResizeTouchStart={handleSplitResizeTouchStart("files")}
|
onResizeMouseDown={handleSplitResizeMouseDown("files")}
|
||||||
isPhoneLayout={props.isPhoneLayout}
|
onResizeTouchStart={handleSplitResizeTouchStart("files")}
|
||||||
/>
|
isPhoneLayout={props.isPhoneLayout}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={rightPanelTab() === "status"}>
|
<Show when={rightPanelTab() === "status"}>
|
||||||
<StatusTab
|
<Suspense fallback={<RightPanelTabFallback />}>
|
||||||
t={props.t}
|
<LazyStatusTab
|
||||||
instanceId={props.instanceId}
|
t={props.t}
|
||||||
instance={props.instance}
|
instanceId={props.instanceId}
|
||||||
activeSessionId={props.activeSessionId}
|
instance={props.instance}
|
||||||
activeSession={props.activeSession}
|
activeSessionId={props.activeSessionId}
|
||||||
activeSessionDiffs={props.activeSessionDiffs}
|
activeSession={props.activeSession}
|
||||||
latestTodoState={props.latestTodoState}
|
activeSessionDiffs={props.activeSessionDiffs}
|
||||||
backgroundProcessList={props.backgroundProcessList}
|
latestTodoState={props.latestTodoState}
|
||||||
onOpenBackgroundOutput={props.onOpenBackgroundOutput}
|
backgroundProcessList={props.backgroundProcessList}
|
||||||
onStopBackgroundProcess={props.onStopBackgroundProcess}
|
onOpenBackgroundOutput={props.onOpenBackgroundOutput}
|
||||||
onTerminateBackgroundProcess={props.onTerminateBackgroundProcess}
|
onStopBackgroundProcess={props.onStopBackgroundProcess}
|
||||||
expandedItems={rightPanelExpandedItems}
|
onTerminateBackgroundProcess={props.onTerminateBackgroundProcess}
|
||||||
onExpandedItemsChange={handleAccordionChange}
|
expandedItems={rightPanelExpandedItems}
|
||||||
onOpenChangesTab={openChangesTabFromStatus}
|
onExpandedItemsChange={handleAccordionChange}
|
||||||
/>
|
onOpenChangesTab={openChangesTabFromStatus}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Component } from "solid-js"
|
|||||||
|
|
||||||
import { AlignJustify, FoldVertical, Split, UnfoldVertical, WrapText } from "lucide-solid"
|
import { AlignJustify, FoldVertical, Split, UnfoldVertical, WrapText } from "lucide-solid"
|
||||||
|
|
||||||
|
import { useI18n } from "../../../../../lib/i18n"
|
||||||
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface DiffToolbarProps {
|
interface DiffToolbarProps {
|
||||||
@@ -14,14 +15,15 @@ interface DiffToolbarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DiffToolbar: Component<DiffToolbarProps> = (props) => {
|
const DiffToolbar: Component<DiffToolbarProps> = (props) => {
|
||||||
|
const { t } = useI18n()
|
||||||
const nextViewMode = (): DiffViewMode => (props.viewMode === "split" ? "unified" : "split")
|
const nextViewMode = (): DiffViewMode => (props.viewMode === "split" ? "unified" : "split")
|
||||||
const nextContextMode = (): DiffContextMode => (props.contextMode === "collapsed" ? "expanded" : "collapsed")
|
const nextContextMode = (): DiffContextMode => (props.contextMode === "collapsed" ? "expanded" : "collapsed")
|
||||||
const nextWordWrapMode = (): DiffWordWrapMode => (props.wordWrapMode === "on" ? "off" : "on")
|
const nextWordWrapMode = (): DiffWordWrapMode => (props.wordWrapMode === "on" ? "off" : "on")
|
||||||
|
|
||||||
const viewModeTitle = () => (nextViewMode() === "split" ? "Switch to split view" : "Switch to unified view")
|
const viewModeTitle = () => (nextViewMode() === "split" ? t("instanceShell.diff.switchToSplit") : t("instanceShell.diff.switchToUnified"))
|
||||||
const contextModeTitle = () =>
|
const contextModeTitle = () =>
|
||||||
nextContextMode() === "collapsed" ? "Hide unchanged regions" : "Show full file"
|
nextContextMode() === "collapsed" ? t("instanceShell.diff.hideUnchanged") : t("instanceShell.diff.showFull")
|
||||||
const wordWrapTitle = () => (nextWordWrapMode() === "on" ? "Enable word wrap" : "Disable word wrap")
|
const wordWrapTitle = () => (nextWordWrapMode() === "on" ? t("instanceShell.diff.enableWordWrap") : t("instanceShell.diff.disableWordWrap"))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="file-viewer-toolbar">
|
<div class="file-viewer-toolbar">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Show, type Component, type JSX } from "solid-js"
|
import { Show, type Component, type JSX } from "solid-js"
|
||||||
|
|
||||||
|
import { useI18n } from "../../../../../lib/i18n"
|
||||||
import OverlayList from "./OverlayList"
|
import OverlayList from "./OverlayList"
|
||||||
|
|
||||||
type SplitFilePanelList = {
|
type SplitFilePanelList = {
|
||||||
@@ -24,12 +25,13 @@ interface SplitFilePanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SplitFilePanel: Component<SplitFilePanelProps> = (props) => {
|
const SplitFilePanel: Component<SplitFilePanelProps> = (props) => {
|
||||||
|
const { t } = useI18n()
|
||||||
return (
|
return (
|
||||||
<div class="files-tab-container">
|
<div class="files-tab-container">
|
||||||
<div class="files-tab-header">
|
<div class="files-tab-header">
|
||||||
<div class="files-tab-header-row">
|
<div class="files-tab-header-row">
|
||||||
<button type="button" class="files-toggle-button" onClick={props.onToggleList}>
|
<button type="button" class="files-toggle-button" onClick={props.onToggleList}>
|
||||||
{props.listOpen ? "Hide files" : "Show files"}
|
{props.listOpen ? t("instanceShell.filesShell.hideFiles") : t("instanceShell.filesShell.showFiles")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{props.header}
|
{props.header}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { For, Show, createMemo, type Accessor, type Component, type JSX } from "solid-js"
|
import { For, Show, Suspense, createMemo, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
||||||
|
|
||||||
import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
|
||||||
|
|
||||||
import DiffToolbar from "../components/DiffToolbar"
|
import DiffToolbar from "../components/DiffToolbar"
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
|
const LazyMonacoDiffViewer = lazy(() =>
|
||||||
|
import("../../../../file-viewer/monaco-diff-viewer").then((module) => ({ default: module.MonacoDiffViewer })),
|
||||||
|
)
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
|
|
||||||
@@ -113,15 +115,23 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(file) => (
|
{(file) => (
|
||||||
<MonacoDiffViewer
|
<Suspense
|
||||||
scopeKey={scopeKey()}
|
fallback={
|
||||||
path={String(file().file || "")}
|
<div class="file-viewer-empty">
|
||||||
before={String((file() as any).before || "")}
|
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||||
after={String((file() as any).after || "")}
|
</div>
|
||||||
viewMode={props.diffViewMode()}
|
}
|
||||||
contextMode={props.diffContextMode()}
|
>
|
||||||
wordWrap={props.diffWordWrapMode()}
|
<LazyMonacoDiffViewer
|
||||||
/>
|
scopeKey={scopeKey()}
|
||||||
|
path={String(file().file || "")}
|
||||||
|
before={String((file() as any).before || "")}
|
||||||
|
after={String((file() as any).after || "")}
|
||||||
|
viewMode={props.diffViewMode()}
|
||||||
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrap={props.diffWordWrapMode()}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +230,7 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
onResizeMouseDown={props.onResizeMouseDown}
|
onResizeMouseDown={props.onResizeMouseDown}
|
||||||
onResizeTouchStart={props.onResizeTouchStart}
|
onResizeTouchStart={props.onResizeTouchStart}
|
||||||
isPhoneLayout={props.isPhoneLayout()}
|
isPhoneLayout={props.isPhoneLayout()}
|
||||||
overlayAriaLabel="Changes"
|
overlayAriaLabel={props.t("instanceShell.rightPanel.tabs.changes")}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { For, Show, type Accessor, type Component, type JSX } from "solid-js"
|
import { For, Show, Suspense, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
||||||
import type { FileNode } from "@opencode-ai/sdk/v2/client"
|
import type { FileNode } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
import { RefreshCw } from "lucide-solid"
|
import { RefreshCw } from "lucide-solid"
|
||||||
|
|
||||||
import { MonacoFileViewer } from "../../../../file-viewer/monaco-file-viewer"
|
|
||||||
|
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
|
|
||||||
|
const LazyMonacoFileViewer = lazy(() =>
|
||||||
|
import("../../../../file-viewer/monaco-file-viewer").then((module) => ({ default: module.MonacoFileViewer })),
|
||||||
|
)
|
||||||
|
|
||||||
interface FilesTabProps {
|
interface FilesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
|
|
||||||
@@ -51,8 +53,8 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
const headerDisplayedPath = () => props.browserSelectedPath() || props.browserPath()
|
const headerDisplayedPath = () => props.browserSelectedPath() || props.browserPath()
|
||||||
|
|
||||||
const emptyViewerMessage = () => {
|
const emptyViewerMessage = () => {
|
||||||
if (props.browserLoading() && entriesValue === null) return "Loading files..."
|
if (props.browserLoading() && entriesValue === null) return props.t("instanceInfo.loading")
|
||||||
return "Select a file to preview"
|
return props.t("instanceShell.filesShell.viewerEmpty")
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
@@ -77,7 +79,15 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(payload) => (
|
{(payload) => (
|
||||||
<MonacoFileViewer scopeKey={props.scopeKey()} path={payload().path} content={payload().content} />
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div class="file-viewer-empty">
|
||||||
|
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LazyMonacoFileViewer scopeKey={props.scopeKey()} path={payload().path} content={payload().content} />
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
@@ -91,7 +101,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="file-viewer-empty">
|
<div class="file-viewer-empty">
|
||||||
<span class="file-viewer-empty-text">Loading…</span>
|
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +123,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.browserLoading() && entriesValue === null}>
|
<Show when={props.browserLoading() && entriesValue === null}>
|
||||||
<div class="p-3 text-xs text-secondary">Loading files...</div>
|
<div class="p-3 text-xs text-secondary">{props.t("instanceInfo.loading")}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<For each={sorted}>
|
<For each={sorted}>
|
||||||
@@ -154,7 +164,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<Show when={props.browserLoading()}>
|
<Show when={props.browserLoading()}>
|
||||||
<span>Loading…</span>
|
<span>{props.t("instanceInfo.loading")}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.browserError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
<Show when={props.browserError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,7 +175,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||||
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||||
disabled={props.browserLoading()}
|
disabled={props.browserLoading()}
|
||||||
style={{ "margin-left": "auto" }}
|
style={{ "margin-inline-start": "auto" }}
|
||||||
onClick={() => props.onRefresh()}
|
onClick={() => props.onRefresh()}
|
||||||
>
|
>
|
||||||
<RefreshCw class={`h-4 w-4${props.browserLoading() ? " animate-spin" : ""}`} />
|
<RefreshCw class={`h-4 w-4${props.browserLoading() ? " animate-spin" : ""}`} />
|
||||||
@@ -180,7 +190,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
onResizeMouseDown={props.onResizeMouseDown}
|
onResizeMouseDown={props.onResizeMouseDown}
|
||||||
onResizeTouchStart={props.onResizeTouchStart}
|
onResizeTouchStart={props.onResizeTouchStart}
|
||||||
isPhoneLayout={props.isPhoneLayout()}
|
isPhoneLayout={props.isPhoneLayout()}
|
||||||
overlayAriaLabel="Files"
|
overlayAriaLabel={props.t("instanceShell.rightPanel.tabs.files")}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { For, Show, createMemo, type Accessor, type Component, type JSX } from "solid-js"
|
import { For, Show, Suspense, createMemo, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
||||||
import type { File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
import type { File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
import { RefreshCw } from "lucide-solid"
|
import { RefreshCw } from "lucide-solid"
|
||||||
|
|
||||||
import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
|
||||||
|
|
||||||
import DiffToolbar from "../components/DiffToolbar"
|
import DiffToolbar from "../components/DiffToolbar"
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
|
const LazyMonacoDiffViewer = lazy(() =>
|
||||||
|
import("../../../../file-viewer/monaco-diff-viewer").then((module) => ({ default: module.MonacoDiffViewer })),
|
||||||
|
)
|
||||||
|
|
||||||
interface GitChangesTabProps {
|
interface GitChangesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
|
|
||||||
@@ -80,11 +82,11 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emptyViewerMessage = createMemo(() => {
|
const emptyViewerMessage = createMemo(() => {
|
||||||
if (!hasSession()) return "Select a session to view changes."
|
if (!hasSession()) return props.t("instanceShell.gitChanges.noSessionSelected")
|
||||||
const currentEntries = entries()
|
const currentEntries = entries()
|
||||||
if (currentEntries === null) return "Loading git changes…"
|
if (currentEntries === null) return props.t("instanceShell.gitChanges.loading")
|
||||||
if (nonDeleted().length === 0) return "No git changes yet."
|
if (nonDeleted().length === 0) return props.t("instanceShell.gitChanges.empty")
|
||||||
return "No file selected."
|
return props.t("instanceShell.filesShell.viewerEmpty")
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderContent = (): JSX.Element => {
|
const renderContent = (): JSX.Element => {
|
||||||
@@ -122,7 +124,14 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(file) => (
|
{(file) => (
|
||||||
<MonacoDiffViewer
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div class="file-viewer-empty">
|
||||||
|
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LazyMonacoDiffViewer
|
||||||
scopeKey={props.scopeKey()}
|
scopeKey={props.scopeKey()}
|
||||||
path={String(file().path || "")}
|
path={String(file().path || "")}
|
||||||
before={String((file() as any).before || "")}
|
before={String((file() as any).before || "")}
|
||||||
@@ -131,7 +140,8 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
wordWrap={props.diffWordWrapMode()}
|
wordWrap={props.diffWordWrapMode()}
|
||||||
/>
|
/>
|
||||||
)}
|
</Suspense>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -144,7 +154,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="file-viewer-empty">
|
<div class="file-viewer-empty">
|
||||||
<span class="file-viewer-empty-text">Loading…</span>
|
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +179,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="file-list-item-stats">
|
<div class="file-list-item-stats">
|
||||||
<Show when={item.status === "deleted"}>
|
<Show when={item.status === "deleted"}>
|
||||||
<span class="text-[10px] text-secondary">deleted</span>
|
<span class="text-[10px] text-secondary">{props.t("instanceShell.gitChanges.deleted")}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={item.status !== "deleted"}>
|
<Show when={item.status !== "deleted"}>
|
||||||
<>
|
<>
|
||||||
@@ -200,7 +210,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="file-list-item-stats">
|
<div class="file-list-item-stats">
|
||||||
<Show when={item.status === "deleted"}>
|
<Show when={item.status === "deleted"}>
|
||||||
<span class="text-[10px] text-secondary">deleted</span>
|
<span class="text-[10px] text-secondary">{props.t("instanceShell.gitChanges.deleted")}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={item.status !== "deleted"}>
|
<Show when={item.status !== "deleted"}>
|
||||||
<>
|
<>
|
||||||
@@ -220,8 +230,8 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
<SplitFilePanel
|
<SplitFilePanel
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<span class="files-tab-selected-path" title={selected?.path || "Git Changes"}>
|
<span class="files-tab-selected-path" title={selected?.path || props.t("instanceShell.rightPanel.tabs.gitChanges")}>
|
||||||
<span class="file-path-text">{selected?.path || "Git Changes"}</span>
|
<span class="file-path-text">{selected?.path || props.t("instanceShell.rightPanel.tabs.gitChanges")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
|
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
|
||||||
@@ -264,7 +274,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
onResizeMouseDown={props.onResizeMouseDown}
|
onResizeMouseDown={props.onResizeMouseDown}
|
||||||
onResizeTouchStart={props.onResizeTouchStart}
|
onResizeTouchStart={props.onResizeTouchStart}
|
||||||
isPhoneLayout={props.isPhoneLayout()}
|
isPhoneLayout={props.isPhoneLayout()}
|
||||||
overlayAriaLabel="Git Changes"
|
overlayAriaLabel={props.t("instanceShell.rightPanel.tabs.gitChanges")}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ export function useDrawerResize(options: DrawerResizeOptions): DrawerResizeApi {
|
|||||||
if (!side) return
|
if (!side) return
|
||||||
const startWidth = resizeStartWidth()
|
const startWidth = resizeStartWidth()
|
||||||
const clamp = side === "left" ? options.clampLeft : options.clampRight
|
const clamp = side === "left" ? options.clampLeft : options.clampRight
|
||||||
const delta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX
|
const isRtl = typeof document !== "undefined" && document.documentElement.dir === "rtl"
|
||||||
|
const rawDelta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX
|
||||||
|
const delta = isRtl ? -rawDelta : rawDelta
|
||||||
const nextWidth = clamp(startWidth + delta)
|
const nextWidth = clamp(startWidth + delta)
|
||||||
applyDrawerWidth(side, nextWidth)
|
applyDrawerWidth(side, nextWidth)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||||
import { renderMarkdown, onLanguagesLoaded, decodeHtmlEntities, setMarkdownTheme } from "../lib/markdown"
|
|
||||||
import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
||||||
import type { TextPart, RenderCache } from "../types/message"
|
import type { TextPart, RenderCache } from "../types/message"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
@@ -8,6 +7,20 @@ import { useI18n } from "../lib/i18n"
|
|||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
type MarkdownModule = typeof import("../lib/markdown")
|
||||||
|
|
||||||
|
let markdownModulePromise: Promise<MarkdownModule> | null = null
|
||||||
|
|
||||||
|
function loadMarkdownModule(): Promise<MarkdownModule> {
|
||||||
|
if (!markdownModulePromise) {
|
||||||
|
markdownModulePromise = import("../lib/markdown").catch((error) => {
|
||||||
|
markdownModulePromise = null
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return markdownModulePromise
|
||||||
|
}
|
||||||
|
|
||||||
function hashText(value: string): string {
|
function hashText(value: string): string {
|
||||||
let hash = 2166136261
|
let hash = 2166136261
|
||||||
for (let index = 0; index < value.length; index++) {
|
for (let index = 0; index < value.length; index++) {
|
||||||
@@ -24,6 +37,45 @@ function resolvePartVersion(part: TextPart, text: string): string {
|
|||||||
return `text-${hashText(text)}`
|
return `text-${hashText(text)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePartCacheId(part: TextPart, text: string): string {
|
||||||
|
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : ""
|
||||||
|
if (partId) {
|
||||||
|
return partId
|
||||||
|
}
|
||||||
|
|
||||||
|
return `anonymous:${hashText(text)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeHtmlEntitiesLocally(content: string): string {
|
||||||
|
if (!content.includes("&") || typeof document === "undefined") {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea")
|
||||||
|
textarea.innerHTML = content
|
||||||
|
return textarea.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(content: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """,
|
||||||
|
"'": "'",
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.replace(/[&<>"']/g, (match) => map[match] ?? match)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFallbackHtml(content: string): string {
|
||||||
|
if (!content) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapeHtml(content).replace(/\n/g, "<br />")
|
||||||
|
}
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
part: TextPart
|
part: TextPart
|
||||||
instanceId?: string
|
instanceId?: string
|
||||||
@@ -38,7 +90,8 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [html, setHtml] = createSignal("")
|
const [html, setHtml] = createSignal("")
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
let latestRequestedText = ""
|
let latestRequestKey = ""
|
||||||
|
let cleanupLanguageListener: (() => void) | undefined
|
||||||
|
|
||||||
const notifyRendered = () => {
|
const notifyRendered = () => {
|
||||||
Promise.resolve().then(() => props.onRendered?.())
|
Promise.resolve().then(() => props.onRendered?.())
|
||||||
@@ -47,15 +100,14 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
const resolved = createMemo(() => {
|
const resolved = createMemo(() => {
|
||||||
const part = props.part
|
const part = props.part
|
||||||
const rawText = typeof part.text === "string" ? part.text : ""
|
const rawText = typeof part.text === "string" ? part.text : ""
|
||||||
const text = decodeHtmlEntities(rawText)
|
const text = decodeHtmlEntitiesLocally(rawText)
|
||||||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||||
const highlightEnabled = !props.disableHighlight
|
const highlightEnabled = !props.disableHighlight
|
||||||
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : ""
|
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
||||||
if (!partId) {
|
const cacheId = resolvePartCacheId(part, text)
|
||||||
throw new Error("Markdown rendering requires a part id")
|
|
||||||
}
|
|
||||||
const version = resolvePartVersion(part, text)
|
const version = resolvePartVersion(part, text)
|
||||||
return { part, text, themeKey, highlightEnabled, partId, version }
|
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${version}`
|
||||||
|
return { part, text, themeKey, highlightEnabled, partId, cacheId, version, requestKey }
|
||||||
})
|
})
|
||||||
|
|
||||||
const cacheHandle = useGlobalCache({
|
const cacheHandle = useGlobalCache({
|
||||||
@@ -63,26 +115,46 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
sessionId: () => props.sessionId,
|
sessionId: () => props.sessionId,
|
||||||
scope: "markdown",
|
scope: "markdown",
|
||||||
cacheId: () => {
|
cacheId: () => {
|
||||||
const { partId, themeKey, highlightEnabled } = resolved()
|
const { cacheId, themeKey, highlightEnabled } = resolved()
|
||||||
return `${partId}:${themeKey}:${highlightEnabled ? 1 : 0}`
|
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}`
|
||||||
},
|
},
|
||||||
version: () => resolved().version,
|
version: () => resolved().version,
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(async () => {
|
const commitCacheEntry = (snapshot: ReturnType<typeof resolved>, renderedHtml: string) => {
|
||||||
const { part, text, themeKey, highlightEnabled, version } = resolved()
|
const cacheEntry: RenderCache = {
|
||||||
|
text: snapshot.text,
|
||||||
|
html: renderedHtml,
|
||||||
|
theme: snapshot.themeKey,
|
||||||
|
mode: snapshot.version,
|
||||||
|
}
|
||||||
|
setHtml(renderedHtml)
|
||||||
|
cacheHandle.set(cacheEntry)
|
||||||
|
notifyRendered()
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the markdown highlighter theme matches the active UI theme.
|
const renderSnapshot = async (snapshot: ReturnType<typeof resolved>) => {
|
||||||
setMarkdownTheme(themeKey === "dark")
|
const markdown = await loadMarkdownModule()
|
||||||
|
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
|
||||||
|
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
||||||
|
suppressHighlight: !snapshot.highlightEnabled,
|
||||||
|
})
|
||||||
|
|
||||||
latestRequestedText = text
|
if (latestRequestKey === snapshot.requestKey) {
|
||||||
|
commitCacheEntry(snapshot, rendered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const snapshot = resolved()
|
||||||
|
latestRequestKey = snapshot.requestKey
|
||||||
|
|
||||||
const cacheMatches = (cache: RenderCache | undefined) => {
|
const cacheMatches = (cache: RenderCache | undefined) => {
|
||||||
if (!cache) return false
|
if (!cache) return false
|
||||||
return cache.theme === themeKey && cache.mode === version
|
return cache.theme === snapshot.themeKey && cache.mode === snapshot.version
|
||||||
}
|
}
|
||||||
|
|
||||||
const localCache = part.renderCache
|
const localCache = snapshot.part.renderCache
|
||||||
if (localCache && cacheMatches(localCache)) {
|
if (localCache && cacheMatches(localCache)) {
|
||||||
setHtml(localCache.html)
|
setHtml(localCache.html)
|
||||||
notifyRendered()
|
notifyRendered()
|
||||||
@@ -96,111 +168,83 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitCacheEntry = (renderedHtml: string) => {
|
setHtml(renderFallbackHtml(snapshot.text))
|
||||||
const cacheEntry: RenderCache = { text, html: renderedHtml, theme: themeKey, mode: version }
|
notifyRendered()
|
||||||
setHtml(renderedHtml)
|
|
||||||
cacheHandle.set(cacheEntry)
|
|
||||||
notifyRendered()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!highlightEnabled) {
|
void renderSnapshot(snapshot).catch((error) => {
|
||||||
try {
|
|
||||||
const rendered = await renderMarkdown(text, { suppressHighlight: true })
|
|
||||||
|
|
||||||
if (latestRequestedText === text) {
|
|
||||||
commitCacheEntry(rendered)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to render markdown:", error)
|
|
||||||
if (latestRequestedText === text) {
|
|
||||||
commitCacheEntry(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rendered = await renderMarkdown(text)
|
|
||||||
if (latestRequestedText === text) {
|
|
||||||
commitCacheEntry(rendered)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to render markdown:", error)
|
log.error("Failed to render markdown:", error)
|
||||||
if (latestRequestedText === text) {
|
if (latestRequestKey === snapshot.requestKey) {
|
||||||
commitCacheEntry(text)
|
commitCacheEntry(snapshot, renderFallbackHtml(snapshot.text))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handleClick = async (e: Event) => {
|
const handleClick = async (event: Event) => {
|
||||||
const target = e.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
|
const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
|
||||||
|
|
||||||
if (copyButton) {
|
if (!copyButton) {
|
||||||
e.preventDefault()
|
return
|
||||||
const code = copyButton.getAttribute("data-code")
|
|
||||||
if (code) {
|
|
||||||
const decodedCode = decodeURIComponent(code)
|
|
||||||
const success = await copyToClipboard(decodedCode)
|
|
||||||
const copyText = copyButton.querySelector(".copy-text")
|
|
||||||
if (copyText) {
|
|
||||||
if (success) {
|
|
||||||
copyText.textContent = t("markdown.codeBlock.copy.copied")
|
|
||||||
setTimeout(() => {
|
|
||||||
copyText.textContent = t("markdown.codeBlock.copy.label")
|
|
||||||
}, 2000)
|
|
||||||
} else {
|
|
||||||
copyText.textContent = t("markdown.codeBlock.copy.failed")
|
|
||||||
setTimeout(() => {
|
|
||||||
copyText.textContent = t("markdown.codeBlock.copy.label")
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
const code = copyButton.getAttribute("data-code")
|
||||||
|
if (!code) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedCode = decodeURIComponent(code)
|
||||||
|
const success = await copyToClipboard(decodedCode)
|
||||||
|
const copyText = copyButton.querySelector(".copy-text")
|
||||||
|
if (!copyText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copyText.textContent = success ? t("markdown.codeBlock.copy.copied") : t("markdown.codeBlock.copy.failed")
|
||||||
|
setTimeout(() => {
|
||||||
|
copyText.textContent = t("markdown.codeBlock.copy.label")
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
containerRef?.addEventListener("click", handleClick)
|
containerRef?.addEventListener("click", handleClick)
|
||||||
|
|
||||||
const cleanupLanguageListener = onLanguagesLoaded(async () => {
|
let disposed = false
|
||||||
if (props.disableHighlight) {
|
void loadMarkdownModule()
|
||||||
return
|
.then((markdown) => {
|
||||||
}
|
if (disposed) {
|
||||||
|
return
|
||||||
const { part, text, themeKey, version } = resolved()
|
|
||||||
|
|
||||||
setMarkdownTheme(themeKey === "dark")
|
|
||||||
|
|
||||||
if (latestRequestedText !== text) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rendered = await renderMarkdown(text)
|
|
||||||
if (latestRequestedText === text) {
|
|
||||||
const cacheEntry: RenderCache = { text, html: rendered, theme: themeKey, mode: version }
|
|
||||||
setHtml(rendered)
|
|
||||||
cacheHandle.set(cacheEntry)
|
|
||||||
notifyRendered()
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to re-render markdown after language load:", error)
|
cleanupLanguageListener = markdown.onLanguagesLoaded(() => {
|
||||||
}
|
const snapshot = resolved()
|
||||||
})
|
if (!snapshot.highlightEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
latestRequestKey = snapshot.requestKey
|
||||||
|
void renderSnapshot(snapshot).catch((error) => {
|
||||||
|
log.error("Failed to re-render markdown after language load:", error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
log.error("Failed to load markdown module:", error)
|
||||||
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
disposed = true
|
||||||
containerRef?.removeEventListener("click", handleClick)
|
containerRef?.removeEventListener("click", handleClick)
|
||||||
cleanupLanguageListener()
|
cleanupLanguageListener?.()
|
||||||
|
cleanupLanguageListener = undefined
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const proseClass = () => "markdown-body"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
class={proseClass()}
|
class="markdown-body"
|
||||||
|
dir="auto"
|
||||||
data-view="markdown"
|
data-view="markdown"
|
||||||
data-part-id={resolved().partId}
|
data-part-id={resolved().partId}
|
||||||
data-markdown-theme={resolved().themeKey}
|
data-markdown-theme={resolved().themeKey}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
import { For, Match, Show, Suspense, Switch, createEffect, createMemo, createSignal, lazy, onCleanup, untrack } from "solid-js"
|
||||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
|
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
|
||||||
import MessageItem from "./message-item"
|
import MessageItem from "./message-item"
|
||||||
import ToolCall from "./tool-call"
|
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
import type { ClientPart, MessageInfo } from "../types/message"
|
import type { ClientPart, MessageInfo } from "../types/message"
|
||||||
import { partHasRenderableText } from "../types/message"
|
import { partHasRenderableText } from "../types/message"
|
||||||
@@ -29,6 +28,12 @@ const USER_BORDER_COLOR = "var(--message-user-border)"
|
|||||||
const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)"
|
const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)"
|
||||||
const TOOL_BORDER_COLOR = "var(--message-tool-border)"
|
const TOOL_BORDER_COLOR = "var(--message-tool-border)"
|
||||||
|
|
||||||
|
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||||
|
|
||||||
|
function ToolCallFallback() {
|
||||||
|
return <div class="tool-call tool-call-loading" />
|
||||||
|
}
|
||||||
|
|
||||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
|
|
||||||
|
|
||||||
@@ -500,16 +505,18 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolCall
|
<Suspense fallback={<ToolCallFallback />}>
|
||||||
toolCall={resolvedToolPart()}
|
<LazyToolCall
|
||||||
toolCallId={props.partId}
|
toolCall={resolvedToolPart()}
|
||||||
messageId={props.messageId}
|
toolCallId={props.partId}
|
||||||
messageVersion={messageVersion()}
|
messageId={props.messageId}
|
||||||
partVersion={partVersion()}
|
messageVersion={messageVersion()}
|
||||||
instanceId={props.instanceId}
|
partVersion={partVersion()}
|
||||||
sessionId={props.sessionId}
|
instanceId={props.instanceId}
|
||||||
onContentRendered={props.onContentRendered}
|
sessionId={props.sessionId}
|
||||||
/>
|
onContentRendered={props.onContentRendered}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -902,6 +909,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
selectedMessageIds={props.selectedMessageIds}
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -1280,6 +1288,7 @@ interface ReasoningCardProps {
|
|||||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
selectedMessageIds?: () => Set<string>
|
selectedMessageIds?: () => Set<string>
|
||||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
|
onContentRendered?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReasoningCard(props: ReasoningCardProps) {
|
function ReasoningCard(props: ReasoningCardProps) {
|
||||||
@@ -1288,6 +1297,25 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||||
|
let pendingRenderNotificationFrame: number | null = null
|
||||||
|
|
||||||
|
const notifyContentRendered = () => {
|
||||||
|
if (!props.onContentRendered || typeof requestAnimationFrame !== "function") return
|
||||||
|
if (pendingRenderNotificationFrame !== null) {
|
||||||
|
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||||
|
}
|
||||||
|
pendingRenderNotificationFrame = requestAnimationFrame(() => {
|
||||||
|
pendingRenderNotificationFrame = null
|
||||||
|
props.onContentRendered?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (pendingRenderNotificationFrame !== null) {
|
||||||
|
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||||
|
pendingRenderNotificationFrame = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setExpanded(Boolean(props.defaultExpanded))
|
setExpanded(Boolean(props.defaultExpanded))
|
||||||
@@ -1356,6 +1384,12 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
const viewHideLabel = () =>
|
const viewHideLabel = () =>
|
||||||
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!expanded()) return
|
||||||
|
reasoningText()
|
||||||
|
notifyContentRendered()
|
||||||
|
})
|
||||||
|
|
||||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
|
|
||||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
@@ -1497,7 +1531,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
<div class="message-reasoning-expanded">
|
<div class="message-reasoning-expanded">
|
||||||
<div class="message-reasoning-body">
|
<div class="message-reasoning-body">
|
||||||
<div class="message-reasoning-output" role="region" aria-label={t("messageBlock.reasoning.detailsAriaLabel")}>
|
<div class="message-reasoning-output" role="region" aria-label={t("messageBlock.reasoning.detailsAriaLabel")}>
|
||||||
<pre class="message-reasoning-text">{reasoningText() || ""}</pre>
|
<pre class="message-reasoning-text" dir="auto">{reasoningText() || ""}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="pt-1 whitespace-pre-wrap break-words leading-[1.1]">
|
<div class="pt-1 whitespace-pre-wrap break-words leading-[1.1]" dir="auto">
|
||||||
|
|
||||||
|
|
||||||
<Show when={props.isQueued && isUser()}>
|
<Show when={props.isQueued && isUser()}>
|
||||||
@@ -550,7 +550,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={errorMessage()}>
|
<Show when={errorMessage()}>
|
||||||
<div class="message-error-block">⚠️ {errorMessage()}</div>
|
<div class="message-error-block" dir="auto">⚠️ {errorMessage()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={isGenerating()}>
|
<Show when={isGenerating()}>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Show, Match, Switch } from "solid-js"
|
import { Match, Show, Suspense, Switch, lazy } from "solid-js"
|
||||||
import ToolCall from "./tool-call"
|
|
||||||
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
@@ -7,6 +6,8 @@ import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/m
|
|||||||
|
|
||||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
|
|
||||||
|
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||||
|
|
||||||
interface MessagePartProps {
|
interface MessagePartProps {
|
||||||
part: ClientPart
|
part: ClientPart
|
||||||
messageType?: "user" | "assistant"
|
messageType?: "user" | "assistant"
|
||||||
@@ -133,11 +134,12 @@ export default function MessagePart(props: MessagePartProps) {
|
|||||||
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||||
<div
|
<div
|
||||||
class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()}
|
class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()}
|
||||||
|
dir="auto"
|
||||||
data-role={textContainerRole()}
|
data-role={textContainerRole()}
|
||||||
data-part-type="text"
|
data-part-type="text"
|
||||||
data-part-id={typeof (props.part as any)?.id === "string" ? (props.part as any).id : undefined}
|
data-part-id={typeof (props.part as any)?.id === "string" ? (props.part as any).id : undefined}
|
||||||
>
|
>
|
||||||
<Show when={canRenderMarkdown()} fallback={<span class="text-primary">{plainTextContent()}</span>}>
|
<Show when={canRenderMarkdown()} fallback={<span class="text-primary" dir="auto">{plainTextContent()}</span>}>
|
||||||
<Markdown
|
<Markdown
|
||||||
part={createTextPartForMarkdown()}
|
part={createTextPartForMarkdown()}
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
@@ -152,12 +154,14 @@ export default function MessagePart(props: MessagePartProps) {
|
|||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={partType() === "tool"}>
|
<Match when={partType() === "tool"}>
|
||||||
<ToolCall
|
<Suspense fallback={<div class="tool-call tool-call-loading" />}>
|
||||||
toolCall={props.part as ToolCallPart}
|
<LazyToolCall
|
||||||
toolCallId={props.part?.id}
|
toolCall={props.part as ToolCallPart}
|
||||||
instanceId={props.instanceId}
|
toolCallId={props.part?.id}
|
||||||
sessionId={props.sessionId}
|
instanceId={props.instanceId}
|
||||||
/>
|
sessionId={props.sessionId}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type { DeleteHoverState } from "../types/delete-hover"
|
|||||||
import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache"
|
import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache"
|
||||||
import { getPartCharCount } from "../lib/token-utils"
|
import { getPartCharCount } from "../lib/token-utils"
|
||||||
|
|
||||||
const SCROLL_SENTINEL_MARGIN_PX = 48
|
const SCROLL_SENTINEL_MARGIN_PX = 8
|
||||||
const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream"
|
const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream"
|
||||||
const QUOTE_SELECTION_MAX_LENGTH = 2000
|
const QUOTE_SELECTION_MAX_LENGTH = 2000
|
||||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
{t("modelSelector.trigger.primary", { model: currentModelValue()?.name ?? t("modelSelector.none") })}
|
{t("modelSelector.trigger.primary", { model: currentModelValue()?.name ?? t("modelSelector.none") })}
|
||||||
</span>
|
</span>
|
||||||
{currentModelValue() && (
|
{currentModelValue() && (
|
||||||
<span class="selector-trigger-secondary">
|
<span class="selector-trigger-secondary" dir="ltr">
|
||||||
{currentModelValue()!.providerId}/{currentModelValue()!.id}
|
{currentModelValue()!.providerId}/{currentModelValue()!.id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, createMemo, createSignal, createEffect, onCleanup, type Component } from "solid-js"
|
import { For, Show, Suspense, createMemo, createSignal, createEffect, lazy, onCleanup, type Component } from "solid-js"
|
||||||
import type { PermissionRequestLike } from "../types/permission"
|
import type { PermissionRequestLike } from "../types/permission"
|
||||||
import { getPermissionCallId, getPermissionDisplayTitle, getPermissionKind, getPermissionMessageId, getPermissionSessionId } from "../types/permission"
|
import { getPermissionCallId, getPermissionDisplayTitle, getPermissionKind, getPermissionMessageId, getPermissionSessionId } from "../types/permission"
|
||||||
import { getQuestionCallId, getQuestionMessageId, getQuestionSessionId, type QuestionRequest } from "../types/question"
|
import { getQuestionCallId, getQuestionMessageId, getQuestionSessionId, type QuestionRequest } from "../types/question"
|
||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
} from "../stores/instances"
|
} from "../stores/instances"
|
||||||
import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions"
|
import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions"
|
||||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||||
import ToolCall from "./tool-call"
|
|
||||||
|
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||||
|
|
||||||
interface PermissionApprovalModalProps {
|
interface PermissionApprovalModalProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
@@ -408,15 +409,17 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(data) => (
|
{(data) => (
|
||||||
<ToolCall
|
<Suspense fallback={<div class="tool-call tool-call-loading" />}>
|
||||||
toolCall={data().toolPart}
|
<LazyToolCall
|
||||||
toolCallId={data().toolPart.id}
|
toolCall={data().toolPart}
|
||||||
messageId={data().messageId}
|
toolCallId={data().toolPart.id}
|
||||||
messageVersion={data().messageVersion}
|
messageId={data().messageId}
|
||||||
partVersion={data().partVersion}
|
messageVersion={data().messageVersion}
|
||||||
instanceId={props.instanceId}
|
partVersion={data().partVersion}
|
||||||
sessionId={data().sessionId}
|
instanceId={props.instanceId}
|
||||||
/>
|
sessionId={data().sessionId}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createSignal, Show, onMount, onCleanup, createEffect, on } from "solid-js"
|
import { Suspense, createEffect, createSignal, lazy, on, onCleanup, onMount, Show } from "solid-js"
|
||||||
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
||||||
import UnifiedPicker from "./unified-picker"
|
|
||||||
import ExpandButton from "./expand-button"
|
import ExpandButton from "./expand-button"
|
||||||
import { clearAttachments, removeAttachment } from "../stores/attachments"
|
import { clearAttachments, removeAttachment } from "../stores/attachments"
|
||||||
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
||||||
|
import { createPastedPlaceholderRegex, pastedDisplayCounterRegex } from "./prompt-input/attachmentPlaceholders"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
import { getActiveInstance } from "../stores/instances"
|
import { getActiveInstance } from "../stores/instances"
|
||||||
import { agents, executeCustomCommand } from "../stores/sessions"
|
import { agents, executeCustomCommand } from "../stores/sessions"
|
||||||
@@ -13,11 +13,41 @@ import { useI18n } from "../lib/i18n"
|
|||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { preferences } from "../stores/preferences"
|
import { preferences } from "../stores/preferences"
|
||||||
import type { ExpandState, PromptInputApi, PromptInputProps, PromptInsertMode, PromptMode } from "./prompt-input/types"
|
import type { ExpandState, PromptInputApi, PromptInputProps, PromptInsertMode, PromptMode } from "./prompt-input/types"
|
||||||
|
import type { Attachment } from "../types/attachment"
|
||||||
import { usePromptState } from "./prompt-input/usePromptState"
|
import { usePromptState } from "./prompt-input/usePromptState"
|
||||||
import { usePromptAttachments } from "./prompt-input/usePromptAttachments"
|
import { usePromptAttachments } from "./prompt-input/usePromptAttachments"
|
||||||
import { usePromptPicker } from "./prompt-input/usePromptPicker"
|
import { usePromptPicker } from "./prompt-input/usePromptPicker"
|
||||||
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
||||||
|
|
||||||
|
function getConsumedPastedTextAttachmentIds(text: string, attachments: Attachment[]): string[] {
|
||||||
|
if (!text || attachments.length === 0) return []
|
||||||
|
|
||||||
|
const usedCounters = new Set<string>()
|
||||||
|
for (const match of text.matchAll(createPastedPlaceholderRegex())) {
|
||||||
|
const counter = match?.[1]
|
||||||
|
if (counter) usedCounters.add(counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedCounters.size === 0) return []
|
||||||
|
|
||||||
|
const consumed = new Set<string>()
|
||||||
|
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
if (!attachment?.id) continue
|
||||||
|
if (attachment?.source?.type !== "text") continue
|
||||||
|
const display = attachment.display
|
||||||
|
if (typeof display !== "string") continue
|
||||||
|
const match = display.match(pastedDisplayCounterRegex)
|
||||||
|
if (!match?.[1]) continue
|
||||||
|
if (usedCounters.has(match[1])) {
|
||||||
|
consumed.add(attachment.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(consumed)
|
||||||
|
}
|
||||||
|
|
||||||
export default function PromptInput(props: PromptInputProps) {
|
export default function PromptInput(props: PromptInputProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -246,7 +276,12 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
commandName.length > 0 &&
|
commandName.length > 0 &&
|
||||||
getCommands(props.instanceId).some((cmd) => cmd.name === commandName)
|
getCommands(props.instanceId).some((cmd) => cmd.name === commandName)
|
||||||
|
|
||||||
const resolvedPrompt = isKnownSlashCommand ? text : resolvePastedPlaceholders(text, currentAttachments)
|
const resolvedCommandArgs = isKnownSlashCommand ? resolvePastedPlaceholders(commandArgs, currentAttachments) : ""
|
||||||
|
const resolvedPrompt = isKnownSlashCommand
|
||||||
|
? resolvedCommandArgs
|
||||||
|
? `${commandToken} ${resolvedCommandArgs}`
|
||||||
|
: commandToken
|
||||||
|
: resolvePastedPlaceholders(text, currentAttachments)
|
||||||
const historyEntry = resolvedPrompt
|
const historyEntry = resolvedPrompt
|
||||||
|
|
||||||
const refreshHistory = () => recordHistoryEntry(historyEntry)
|
const refreshHistory = () => recordHistoryEntry(historyEntry)
|
||||||
@@ -262,6 +297,10 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
syncAttachmentCounters("")
|
syncAttachmentCounters("")
|
||||||
setIgnoredAtPositions(new Set<number>())
|
setIgnoredAtPositions(new Set<number>())
|
||||||
} else {
|
} else {
|
||||||
|
const consumedIds = getConsumedPastedTextAttachmentIds(commandArgs, currentAttachments)
|
||||||
|
for (const attachmentId of consumedIds) {
|
||||||
|
removeAttachment(props.instanceId, props.sessionId, attachmentId)
|
||||||
|
}
|
||||||
syncAttachmentCounters("")
|
syncAttachmentCounters("")
|
||||||
setIgnoredAtPositions(new Set<number>())
|
setIgnoredAtPositions(new Set<number>())
|
||||||
}
|
}
|
||||||
@@ -281,7 +320,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
await props.onSend(resolvedPrompt, [])
|
await props.onSend(resolvedPrompt, [])
|
||||||
}
|
}
|
||||||
} else if (isKnownSlashCommand) {
|
} else if (isKnownSlashCommand) {
|
||||||
await executeCustomCommand(props.instanceId, props.sessionId, commandName, commandArgs)
|
await executeCustomCommand(props.instanceId, props.sessionId, commandName, resolvedCommandArgs)
|
||||||
} else {
|
} else {
|
||||||
await props.onSend(resolvedPrompt, currentAttachments)
|
await props.onSend(resolvedPrompt, currentAttachments)
|
||||||
}
|
}
|
||||||
@@ -428,18 +467,20 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<Show when={showPicker() && instance()}>
|
<Show when={showPicker() && instance()}>
|
||||||
<UnifiedPicker
|
<Suspense fallback={null}>
|
||||||
open={showPicker()}
|
<LazyUnifiedPicker
|
||||||
mode={pickerMode()}
|
open={showPicker()}
|
||||||
onClose={handlePickerClose}
|
mode={pickerMode()}
|
||||||
onSelect={handlePickerSelect}
|
onClose={handlePickerClose}
|
||||||
agents={instanceAgents()}
|
onSelect={handlePickerSelect}
|
||||||
commands={getCommands(props.instanceId)}
|
agents={instanceAgents()}
|
||||||
instanceClient={instance()!.client}
|
commands={getCommands(props.instanceId)}
|
||||||
searchQuery={searchQuery()}
|
instanceClient={instance()!.client}
|
||||||
textareaRef={textareaRef}
|
searchQuery={searchQuery()}
|
||||||
workspaceId={props.instanceId}
|
textareaRef={textareaRef}
|
||||||
/>
|
workspaceId={props.instanceId}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
@@ -449,6 +490,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
class={`prompt-input ${mode() === "shell" ? "shell-mode" : ""} ${expandState() === "expanded" ? "is-expanded" : ""}`}
|
class={`prompt-input ${mode() === "shell" ? "shell-mode" : ""} ${expandState() === "expanded" ? "is-expanded" : ""}`}
|
||||||
|
dir="auto"
|
||||||
placeholder={getPlaceholder()}
|
placeholder={getPlaceholder()}
|
||||||
value={prompt()}
|
value={prompt()}
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{rowProps.isChild ? <Bot class="w-4 h-4 flex-shrink-0" /> : <User class="w-4 h-4 flex-shrink-0" />}
|
{rowProps.isChild ? <Bot class="w-4 h-4 flex-shrink-0" /> : <User class="w-4 h-4 flex-shrink-0" />}
|
||||||
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
<span class="session-item-title session-item-title--clamp" dir="auto">{title()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="session-item-row session-item-meta">
|
<div class="session-item-row session-item-meta">
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const SessionRenameDialog: Component<SessionRenameDialogProps> = (props) => {
|
|||||||
inputRef = element
|
inputRef = element
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
|
dir="auto"
|
||||||
value={title()}
|
value={title()}
|
||||||
onInput={(event) => setTitle(event.currentTarget.value)}
|
onInput={(event) => setTitle(event.currentTarget.value)}
|
||||||
placeholder={t("sessionRenameDialog.input.placeholder")}
|
placeholder={t("sessionRenameDialog.input.placeholder")}
|
||||||
|
|||||||
@@ -514,6 +514,7 @@ function ToolCallDetails(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { renderDiffContent } = createDiffContentRenderer({
|
const { renderDiffContent } = createDiffContentRenderer({
|
||||||
|
toolState: props.toolState,
|
||||||
preferences: props.preferences,
|
preferences: props.preferences,
|
||||||
setDiffViewMode: props.setDiffViewMode,
|
setDiffViewMode: props.setDiffViewMode,
|
||||||
isDark: props.isDark,
|
isDark: props.isDark,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Accessor, JSXElement } from "solid-js"
|
import type { Accessor, JSXElement } from "solid-js"
|
||||||
import type { RenderCache } from "../../types/message"
|
import type { RenderCache } from "../../types/message"
|
||||||
import { ansiToHtml, createAnsiStreamRenderer, hasAnsi } from "../../lib/ansi"
|
import { ansiToHtml, createAnsiStreamRenderer, hasAnsi } from "../../lib/ansi"
|
||||||
import { escapeHtml } from "../../lib/markdown"
|
import { escapeHtml } from "../../lib/text-render-utils"
|
||||||
import type { AnsiRenderOptions, ToolScrollHelpers } from "./types"
|
import type { AnsiRenderOptions, ToolScrollHelpers } from "./types"
|
||||||
|
|
||||||
type AnsiRenderCache = RenderCache & { hasAnsi: boolean }
|
type AnsiRenderCache = RenderCache & { hasAnsi: boolean }
|
||||||
@@ -20,6 +20,14 @@ export function createAnsiContentRenderer(params: {
|
|||||||
const runningAnsiRenderer = createAnsiStreamRenderer()
|
const runningAnsiRenderer = createAnsiStreamRenderer()
|
||||||
let runningAnsiSource = ""
|
let runningAnsiSource = ""
|
||||||
|
|
||||||
|
const registerTracked = (element: HTMLDivElement | null) => {
|
||||||
|
params.scrollHelpers.registerContainer(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerUntracked = (element: HTMLDivElement | null) => {
|
||||||
|
params.scrollHelpers.registerContainer(element, { disableTracking: true })
|
||||||
|
}
|
||||||
|
|
||||||
const getMode = () => {
|
const getMode = () => {
|
||||||
const version = params.partVersion?.()
|
const version = params.partVersion?.()
|
||||||
return typeof version === "number" ? String(version) : undefined
|
return typeof version === "number" ? String(version) : undefined
|
||||||
@@ -36,6 +44,8 @@ export function createAnsiContentRenderer(params: {
|
|||||||
const cached = cacheHandle.get<AnsiRenderCache>()
|
const cached = cacheHandle.get<AnsiRenderCache>()
|
||||||
const mode = getMode()
|
const mode = getMode()
|
||||||
const isRunningVariant = options.variant === "running"
|
const isRunningVariant = options.variant === "running"
|
||||||
|
const disableScrollTracking = !isRunningVariant
|
||||||
|
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||||
|
|
||||||
let nextCache: AnsiRenderCache
|
let nextCache: AnsiRenderCache
|
||||||
|
|
||||||
@@ -87,9 +97,9 @@ export function createAnsiContentRenderer(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={messageClass} ref={params.scrollHelpers.registerContainer} onScroll={params.scrollHelpers.handleScroll}>
|
<div class={messageClass} ref={registerRef} onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}>
|
||||||
<pre class="tool-call-content tool-call-ansi" innerHTML={nextCache.html} />
|
<pre class="tool-call-content tool-call-ansi" dir="auto" innerHTML={nextCache.html} />
|
||||||
{params.scrollHelpers.renderSentinel()}
|
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function renderDiagnosticsSection(
|
|||||||
{entry.displayPath}
|
{entry.displayPath}
|
||||||
<span class="tool-call-diagnostic-coords">:L{entry.line || "-"}:C{entry.column || "-"}</span>
|
<span class="tool-call-diagnostic-coords">:L{entry.line || "-"}:C{entry.column || "-"}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="tool-call-diagnostic-message">{entry.message}</span>
|
<span class="tool-call-diagnostic-message" dir="auto">{entry.message}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
import type { Accessor, JSXElement } from "solid-js"
|
import { Suspense, lazy, onMount, type Accessor, type JSXElement } from "solid-js"
|
||||||
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { RenderCache } from "../../types/message"
|
import type { RenderCache } from "../../types/message"
|
||||||
import type { DiffViewMode } from "../../stores/preferences"
|
import type { DiffViewMode } from "../../stores/preferences"
|
||||||
import { ToolCallDiffViewer } from "../diff-viewer"
|
|
||||||
import type { DiffPayload, DiffRenderOptions, ToolScrollHelpers } from "./types"
|
import type { DiffPayload, DiffRenderOptions, ToolScrollHelpers } from "./types"
|
||||||
import { getRelativePath } from "./utils"
|
import { getRelativePath } from "./utils"
|
||||||
import { getCacheEntry } from "../../lib/global-cache"
|
import { getCacheEntry } from "../../lib/global-cache"
|
||||||
|
|
||||||
|
const LazyToolCallDiffViewer = lazy(() =>
|
||||||
|
import("../diff-viewer").then((module) => ({ default: module.ToolCallDiffViewer })),
|
||||||
|
)
|
||||||
|
|
||||||
|
function CachedDiffMarkup(props: { html: string; onRendered?: () => void }) {
|
||||||
|
onMount(() => {
|
||||||
|
props.onRendered?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="tool-call-diff-viewer">
|
||||||
|
<div innerHTML={props.html} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type CacheHandle = {
|
type CacheHandle = {
|
||||||
get<T>(): T | undefined
|
get<T>(): T | undefined
|
||||||
params(): unknown
|
params(): unknown
|
||||||
@@ -16,6 +32,7 @@ type DiffPrefs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createDiffContentRenderer(params: {
|
export function createDiffContentRenderer(params: {
|
||||||
|
toolState: Accessor<ToolState | undefined>
|
||||||
preferences: Accessor<DiffPrefs>
|
preferences: Accessor<DiffPrefs>
|
||||||
setDiffViewMode: (mode: DiffViewMode) => void
|
setDiffViewMode: (mode: DiffViewMode) => void
|
||||||
isDark: Accessor<boolean>
|
isDark: Accessor<boolean>
|
||||||
@@ -43,7 +60,10 @@ export function createDiffContentRenderer(params: {
|
|||||||
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
||||||
const diffMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
const diffMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
||||||
const themeKey = params.isDark() ? "dark" : "light"
|
const themeKey = params.isDark() ? "dark" : "light"
|
||||||
const disableScrollTracking = Boolean(options?.disableScrollTracking)
|
const state = params.toolState()
|
||||||
|
const disableScrollTracking = Boolean(
|
||||||
|
options?.disableScrollTracking || (state?.status !== "running" && state?.status !== "pending"),
|
||||||
|
)
|
||||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||||
|
|
||||||
const baseEntryParams = cacheHandle.params() as any
|
const baseEntryParams = cacheHandle.params() as any
|
||||||
@@ -101,15 +121,20 @@ export function createDiffContentRenderer(params: {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToolCallDiffViewer
|
{cachedHtml ? (
|
||||||
diffText={payload.diffText}
|
<CachedDiffMarkup html={cachedHtml} onRendered={handleDiffRendered} />
|
||||||
filePath={payload.filePath}
|
) : (
|
||||||
theme={themeKey}
|
<Suspense fallback={<pre class="tool-call-diff-fallback">{payload.diffText}</pre>}>
|
||||||
mode={diffMode()}
|
<LazyToolCallDiffViewer
|
||||||
cachedHtml={cachedHtml}
|
diffText={payload.diffText}
|
||||||
cacheEntryParams={cacheEntryParams as any}
|
filePath={payload.filePath}
|
||||||
onRendered={handleDiffRendered}
|
theme={themeKey}
|
||||||
/>
|
mode={diffMode()}
|
||||||
|
cacheEntryParams={cacheEntryParams as any}
|
||||||
|
onRendered={handleDiffRendered}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ export function createMarkdownContentRenderer(params: {
|
|||||||
const size = options.size || "default"
|
const size = options.size || "default"
|
||||||
const disableHighlight = options.disableHighlight || false
|
const disableHighlight = options.disableHighlight || false
|
||||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
||||||
const disableScrollTracking = options.disableScrollTracking || false
|
|
||||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
|
||||||
|
|
||||||
const state = params.toolState()
|
const state = params.toolState()
|
||||||
|
const disableScrollTracking = options.disableScrollTracking || (state?.status !== "running" && state?.status !== "pending")
|
||||||
|
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||||
const shouldDeferMarkdown = Boolean(state && (state.status === "running" || state.status === "pending") && disableHighlight)
|
const shouldDeferMarkdown = Boolean(state && (state.status === "running" || state.status === "pending") && disableHighlight)
|
||||||
if (shouldDeferMarkdown) {
|
if (shouldDeferMarkdown) {
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +42,7 @@ export function createMarkdownContentRenderer(params: {
|
|||||||
ref={registerRef}
|
ref={registerRef}
|
||||||
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
||||||
>
|
>
|
||||||
<pre class="whitespace-pre-wrap break-words text-sm font-mono">{options.content}</pre>
|
<pre class="whitespace-pre-wrap break-words text-sm font-mono" dir="auto">{options.content}</pre>
|
||||||
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isRenderableDiffText } from "../../lib/diff-utils"
|
import { isRenderableDiffText } from "../../lib/diff-utils"
|
||||||
import { getLanguageFromPath } from "../../lib/markdown"
|
import { getLanguageFromPath } from "../../lib/text-render-utils"
|
||||||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { DiffPayload } from "./types"
|
import type { DiffPayload } from "./types"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Index, Show, createEffect, createMemo, createSignal, onCleanup, type Accessor, type JSX } from "solid-js"
|
import { Show, createEffect, createMemo, createSignal, onCleanup, type Accessor, type JSX, on } from "solid-js"
|
||||||
import VirtualItem, { type VirtualItemHeightChangeMeta } from "./virtual-item"
|
import { Virtualizer, type VirtualizerHandle } from "virtua/solid"
|
||||||
|
|
||||||
const DEFAULT_SCROLL_SENTINEL_MARGIN_PX = 48
|
const DEFAULT_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||||
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
||||||
@@ -122,55 +122,28 @@ export interface VirtualFollowListProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||||
const getAnchorId = (key: string) => (props.getAnchorId ? props.getAnchorId(key) : key)
|
|
||||||
const getKeyFromAnchorId = (anchorId: string) => (props.getKeyFromAnchorId ? props.getKeyFromAnchorId(anchorId) : anchorId)
|
|
||||||
|
|
||||||
const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
|
const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [shellElement, setShellElement] = createSignal<HTMLDivElement | undefined>()
|
const [shellElement, setShellElement] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [topSentinel, setTopSentinel] = createSignal<HTMLDivElement | null>(null)
|
const [virtuaHandle, setVirtuaHandle] = createSignal<VirtualizerHandle | undefined>()
|
||||||
const [bottomSentinelSignal, setBottomSentinelSignal] = createSignal<HTMLDivElement | null>(null)
|
|
||||||
const bottomSentinel = () => bottomSentinelSignal()
|
|
||||||
|
|
||||||
const isActive = () => (props.isActive ? props.isActive() : true)
|
const isActive = () => (props.isActive ? props.isActive() : true)
|
||||||
const scrollToBottomOnActivate = () => (props.scrollToBottomOnActivate ? props.scrollToBottomOnActivate() : true)
|
const scrollToBottomOnActivate = () => (props.scrollToBottomOnActivate ? props.scrollToBottomOnActivate() : true)
|
||||||
const initialScrollToBottom = () => (props.initialScrollToBottom ? props.initialScrollToBottom() : true)
|
const initialScrollToBottom = () => (props.initialScrollToBottom ? props.initialScrollToBottom() : true)
|
||||||
const initialAutoScroll = () => (props.initialAutoScroll ? props.initialAutoScroll() : true)
|
const initialAutoScroll = () => (props.initialAutoScroll ? props.initialAutoScroll() : true)
|
||||||
const isLoading = () => Boolean(props.loading?.())
|
|
||||||
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
|
|
||||||
const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())
|
|
||||||
|
|
||||||
const [autoScroll, setAutoScroll] = createSignal(Boolean(initialAutoScroll()))
|
const [autoScroll, setAutoScroll] = createSignal(Boolean(initialAutoScroll()))
|
||||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||||
const [topSentinelVisible, setTopSentinelVisible] = createSignal(true)
|
|
||||||
const [bottomSentinelVisible, setBottomSentinelVisible] = createSignal(true)
|
|
||||||
const [activeKey, setActiveKey] = createSignal<string | null>(null)
|
const [activeKey, setActiveKey] = createSignal<string | null>(null)
|
||||||
|
|
||||||
const [anchorLock, setAnchorLock] = createSignal<{ key: string; block: ScrollLogicalPosition } | null>(null)
|
|
||||||
|
|
||||||
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
||||||
|
|
||||||
let containerRef: HTMLDivElement | undefined
|
let userScrollIntentUntil = 0
|
||||||
let shellRef: HTMLDivElement | undefined
|
let lastUserScrollIntentDirection: "up" | "down" | null = null
|
||||||
let pendingScrollFrame: number | null = null
|
let detachScrollIntentListeners: (() => void) | undefined
|
||||||
let pendingAnchorScroll: number | null = null
|
let lastResetKey: string | number | undefined
|
||||||
let pendingAnchorCorrectionFrame: number | null = null
|
|
||||||
let pendingScrollCompensationScheduled = false
|
|
||||||
let pendingScrollCompensations = new Map<string, number>()
|
|
||||||
let scrollCompensationGen = 0
|
|
||||||
let pendingActiveScroll = false
|
|
||||||
let suppressAutoScrollOnce = false
|
let suppressAutoScrollOnce = false
|
||||||
let pendingInitialScroll = true
|
let pendingInitialScroll = true
|
||||||
let scrollToBottomFrame: number | null = null
|
|
||||||
let scrollToBottomDelayedFrame: number | null = null
|
|
||||||
|
|
||||||
let lastKnownScrollTop = 0
|
|
||||||
let lastUserScrollIntentDirection: "up" | "down" | null = null
|
|
||||||
|
|
||||||
let userScrollIntentUntil = 0
|
|
||||||
let detachScrollIntentListeners: (() => void) | undefined
|
|
||||||
|
|
||||||
let lastResetKey: string | number | undefined
|
|
||||||
|
|
||||||
const state: VirtualFollowListState = {
|
const state: VirtualFollowListState = {
|
||||||
autoScroll,
|
autoScroll,
|
||||||
@@ -181,7 +154,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markUserScrollIntent(direction?: "up" | "down" | null) {
|
function markUserScrollIntent(direction?: "up" | "down" | null) {
|
||||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
const now = performance.now()
|
||||||
userScrollIntentUntil = now + USER_SCROLL_INTENT_WINDOW_MS
|
userScrollIntentUntil = now + USER_SCROLL_INTENT_WINDOW_MS
|
||||||
if (direction) {
|
if (direction) {
|
||||||
lastUserScrollIntentDirection = direction
|
lastUserScrollIntentDirection = direction
|
||||||
@@ -189,8 +162,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasUserScrollIntent() {
|
function hasUserScrollIntent() {
|
||||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
return performance.now() <= userScrollIntentUntil
|
||||||
return now <= userScrollIntentUntil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachScrollIntentListeners(element: HTMLDivElement | undefined) {
|
function attachScrollIntentListeners(element: HTMLDivElement | undefined) {
|
||||||
@@ -231,670 +203,189 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateScrollIndicatorsFromVisibility() {
|
function updateScrollButtons() {
|
||||||
|
const handle = virtuaHandle()
|
||||||
|
const element = scrollElement()
|
||||||
|
if (!handle || !element) return
|
||||||
|
|
||||||
|
const offset = handle.scrollOffset
|
||||||
|
const scrollHeight = handle.scrollSize
|
||||||
|
const clientHeight = element.clientHeight
|
||||||
|
const atBottom = scrollHeight - (offset + clientHeight) <= (props.scrollSentinelMarginPx ?? DEFAULT_SCROLL_SENTINEL_MARGIN_PX)
|
||||||
|
const atTop = offset <= (props.scrollSentinelMarginPx ?? DEFAULT_SCROLL_SENTINEL_MARGIN_PX)
|
||||||
|
|
||||||
const hasItems = props.items().length > 0
|
const hasItems = props.items().length > 0
|
||||||
const bottomVisible = bottomSentinelVisible()
|
setShowScrollBottomButton(hasItems && !atBottom)
|
||||||
const topVisible = topSentinelVisible()
|
setShowScrollTopButton(hasItems && !atTop)
|
||||||
setShowScrollBottomButton(hasItems && !bottomVisible)
|
|
||||||
setShowScrollTopButton(hasItems && !topVisible)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearScrollToBottomFrames() {
|
// Sync autoScroll state based on scroll position if it was a user scroll
|
||||||
if (scrollToBottomFrame !== null) {
|
if (hasUserScrollIntent()) {
|
||||||
cancelAnimationFrame(scrollToBottomFrame)
|
if (atBottom && !autoScroll()) {
|
||||||
scrollToBottomFrame = null
|
setAutoScroll(true)
|
||||||
}
|
} else if (!atBottom && autoScroll()) {
|
||||||
if (scrollToBottomDelayedFrame !== null) {
|
setAutoScroll(false)
|
||||||
cancelAnimationFrame(scrollToBottomDelayedFrame)
|
}
|
||||||
scrollToBottomDelayedFrame = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom(immediate = false, options?: { suppressAutoAnchor?: boolean }) {
|
function scrollToBottom(immediate = true, options?: { suppressAutoAnchor?: boolean }) {
|
||||||
if (!containerRef) return
|
const handle = virtuaHandle()
|
||||||
if (anchorLock()) {
|
if (!handle) return
|
||||||
clearAnchorLock()
|
if (options?.suppressAutoAnchor ?? !immediate) {
|
||||||
}
|
|
||||||
const sentinel = bottomSentinel()
|
|
||||||
const behavior: ScrollBehavior = immediate ? "auto" : "smooth"
|
|
||||||
const suppressAutoAnchor = options?.suppressAutoAnchor ?? !immediate
|
|
||||||
if (suppressAutoAnchor) {
|
|
||||||
suppressAutoScrollOnce = true
|
suppressAutoScrollOnce = true
|
||||||
}
|
}
|
||||||
sentinel?.scrollIntoView({ block: "end", inline: "nearest", behavior })
|
handle.scrollToIndex(props.items().length - 1, { align: "end", smooth: !immediate })
|
||||||
setAutoScroll(true)
|
setAutoScroll(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestScrollToBottom(immediate = true) {
|
function scrollToTop(immediate = true) {
|
||||||
if (!isActive()) {
|
const handle = virtuaHandle()
|
||||||
pendingActiveScroll = true
|
if (!handle) return
|
||||||
return
|
handle.scrollToIndex(0, { align: "start", smooth: !immediate })
|
||||||
}
|
|
||||||
if (!containerRef || !bottomSentinel()) {
|
|
||||||
pendingActiveScroll = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pendingActiveScroll = false
|
|
||||||
clearScrollToBottomFrames()
|
|
||||||
scrollToBottomFrame = requestAnimationFrame(() => {
|
|
||||||
scrollToBottomFrame = null
|
|
||||||
scrollToBottomDelayedFrame = requestAnimationFrame(() => {
|
|
||||||
scrollToBottomDelayedFrame = null
|
|
||||||
scrollToBottom(immediate)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePendingActiveScroll() {
|
|
||||||
if (!pendingActiveScroll) return
|
|
||||||
if (!isActive()) return
|
|
||||||
requestScrollToBottom(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToTop(immediate = false) {
|
|
||||||
if (!containerRef) return
|
|
||||||
const behavior: ScrollBehavior = immediate ? "auto" : "smooth"
|
|
||||||
if (anchorLock()) {
|
|
||||||
clearAnchorLock()
|
|
||||||
}
|
|
||||||
setAutoScroll(false)
|
setAutoScroll(false)
|
||||||
topSentinel()?.scrollIntoView({ block: "start", inline: "nearest", behavior })
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleAnchorScroll(immediate = false) {
|
|
||||||
if (!autoScroll()) return
|
|
||||||
if (!isActive()) {
|
|
||||||
pendingActiveScroll = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sentinel = bottomSentinel()
|
|
||||||
if (!sentinel) {
|
|
||||||
pendingActiveScroll = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (pendingAnchorScroll !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorScroll)
|
|
||||||
pendingAnchorScroll = null
|
|
||||||
}
|
|
||||||
pendingAnchorScroll = requestAnimationFrame(() => {
|
|
||||||
pendingAnchorScroll = null
|
|
||||||
sentinel.scrollIntoView({ block: "end", inline: "nearest", behavior: immediate ? "auto" : "smooth" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAnchorLock() {
|
|
||||||
setAnchorLock(null)
|
|
||||||
if (pendingAnchorCorrectionFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorCorrectionFrame)
|
|
||||||
pendingAnchorCorrectionFrame = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeDesiredOffset(block: ScrollLogicalPosition, container: HTMLElement, anchorRect: DOMRect) {
|
|
||||||
if (block === "end") {
|
|
||||||
return Math.max(0, container.clientHeight - anchorRect.height)
|
|
||||||
}
|
|
||||||
if (block === "center") {
|
|
||||||
return Math.max(0, container.clientHeight / 2 - anchorRect.height / 2)
|
|
||||||
}
|
|
||||||
// Default to start.
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAnchorCorrection() {
|
|
||||||
const lock = anchorLock()
|
|
||||||
if (!lock) return
|
|
||||||
if (autoScroll()) return
|
|
||||||
if (!containerRef) return
|
|
||||||
if (typeof document === "undefined") return
|
|
||||||
|
|
||||||
const anchorId = getAnchorId(lock.key)
|
|
||||||
const anchor = document.getElementById(anchorId)
|
|
||||||
if (!anchor) return
|
|
||||||
|
|
||||||
const containerRect = containerRef.getBoundingClientRect()
|
|
||||||
const anchorRect = anchor.getBoundingClientRect()
|
|
||||||
const currentOffset = anchorRect.top - containerRect.top
|
|
||||||
const desiredOffset = computeDesiredOffset(lock.block, containerRef, anchorRect)
|
|
||||||
const delta = currentOffset - desiredOffset
|
|
||||||
if (!Number.isFinite(delta) || Math.abs(delta) < 0.5) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const nextTop = containerRef.scrollTop + delta
|
|
||||||
const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
|
|
||||||
containerRef.scrollTop = Math.min(maxScrollTop, Math.max(0, nextTop))
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleAnchorCorrection() {
|
|
||||||
if (pendingAnchorCorrectionFrame !== null) return
|
|
||||||
pendingAnchorCorrectionFrame = requestAnimationFrame(() => {
|
|
||||||
pendingAnchorCorrectionFrame = null
|
|
||||||
applyAnchorCorrection()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleContentRendered() {
|
|
||||||
if (autoScroll() && !anchorLock()) {
|
|
||||||
scheduleAutoPinToBottom()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (anchorLock() && !autoScroll()) {
|
|
||||||
scheduleAnchorCorrection()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (!containerRef) return
|
|
||||||
if (pendingScrollFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingScrollFrame)
|
|
||||||
}
|
|
||||||
const isUserScroll = hasUserScrollIntent()
|
const isUserScroll = hasUserScrollIntent()
|
||||||
pendingScrollFrame = requestAnimationFrame(() => {
|
if (isUserScroll) {
|
||||||
pendingScrollFrame = null
|
if (lastUserScrollIntentDirection === "up" && autoScroll()) {
|
||||||
if (!containerRef) return
|
setAutoScroll(false)
|
||||||
const previousScrollTop = lastKnownScrollTop
|
|
||||||
const currentScrollTop = containerRef.scrollTop
|
|
||||||
const deltaScrollTop = currentScrollTop - previousScrollTop
|
|
||||||
if (currentScrollTop !== lastKnownScrollTop) {
|
|
||||||
lastKnownScrollTop = currentScrollTop
|
|
||||||
}
|
}
|
||||||
const atBottom = bottomSentinelVisible()
|
}
|
||||||
|
updateScrollButtons()
|
||||||
|
props.onScroll?.()
|
||||||
|
|
||||||
const beforeAutoScroll = autoScroll()
|
// Find active key (roughly the first visible item)
|
||||||
|
const handle = virtuaHandle()
|
||||||
const inferredDirection: "up" | "down" | null =
|
if (handle) {
|
||||||
lastUserScrollIntentDirection ?? (deltaScrollTop < 0 ? "up" : deltaScrollTop > 0 ? "down" : null)
|
const start = handle.findItemIndex(handle.scrollOffset)
|
||||||
|
const items = props.items()
|
||||||
// If the user scrolls manually, exit key-anchored mode.
|
if (items[start]) {
|
||||||
if (isUserScroll && anchorLock()) {
|
const key = props.getKey(items[start], start)
|
||||||
clearAnchorLock()
|
if (key !== activeKey()) {
|
||||||
}
|
setActiveKey(key)
|
||||||
|
props.onActiveKeyChange?.(key)
|
||||||
if (isUserScroll) {
|
|
||||||
// If the user is actively scrolling upward, exit follow-to-bottom mode
|
|
||||||
// immediately. The bottom sentinel can remain "visible" for a short
|
|
||||||
// distance due to its observer margin, which otherwise keeps autoScroll
|
|
||||||
// enabled and makes the list feel stuck.
|
|
||||||
if (inferredDirection === "up" && deltaScrollTop < -0.5 && autoScroll()) {
|
|
||||||
if (pendingAnchorScroll !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorScroll)
|
|
||||||
pendingAnchorScroll = null
|
|
||||||
}
|
|
||||||
setAutoScroll(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not re-enable follow mode while the user's current scroll intent
|
|
||||||
// is upward. This prevents transient anchor/pin scrolls from pulling
|
|
||||||
// the list back into autoScroll(true).
|
|
||||||
if (inferredDirection !== "up") {
|
|
||||||
if (atBottom) {
|
|
||||||
if (!autoScroll()) setAutoScroll(true)
|
|
||||||
} else if (autoScroll()) {
|
|
||||||
setAutoScroll(false)
|
|
||||||
}
|
|
||||||
} else if (!atBottom && autoScroll()) {
|
|
||||||
// If the user is scrolling up and we are no longer at the bottom,
|
|
||||||
// ensure follow mode is disabled.
|
|
||||||
setAutoScroll(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onScroll?.()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setContainerRef(element: HTMLDivElement | null) {
|
|
||||||
containerRef = element || undefined
|
|
||||||
setScrollElement(containerRef)
|
|
||||||
props.onScrollElementChange?.(containerRef)
|
|
||||||
attachScrollIntentListeners(containerRef)
|
|
||||||
lastKnownScrollTop = containerRef?.scrollTop ?? 0
|
|
||||||
lastUserScrollIntentDirection = null
|
|
||||||
if (!containerRef) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
resolvePendingActiveScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleScrollCompensation(key: string, delta: number) {
|
|
||||||
if (!containerRef) return
|
|
||||||
if (!delta || !Number.isFinite(delta)) return
|
|
||||||
if (typeof document === "undefined") return
|
|
||||||
|
|
||||||
// Only compensate while the user scrolls upward (testing default).
|
|
||||||
if (!hasUserScrollIntent() || lastUserScrollIntentDirection !== "up") return
|
|
||||||
if (autoScroll() || anchorLock()) return
|
|
||||||
|
|
||||||
const anchorId = getAnchorId(key)
|
|
||||||
const anchor = document.getElementById(anchorId)
|
|
||||||
if (!anchor) return
|
|
||||||
const containerRect = containerRef.getBoundingClientRect()
|
|
||||||
const rect = anchor.getBoundingClientRect()
|
|
||||||
// Determine whether the item was fully above the viewport *before* the
|
|
||||||
// height delta applied. Items can expand downward into the viewport; in that
|
|
||||||
// case we still need to compensate to keep existing visible content stable.
|
|
||||||
const bottomAfter = rect.bottom
|
|
||||||
const bottomBefore = bottomAfter - delta
|
|
||||||
const wasAboveViewport = bottomBefore < containerRect.top
|
|
||||||
if (!wasAboveViewport) return
|
|
||||||
|
|
||||||
const next = (pendingScrollCompensations.get(key) ?? 0) + delta
|
|
||||||
pendingScrollCompensations.set(key, next)
|
|
||||||
|
|
||||||
if (pendingScrollCompensationScheduled) return
|
|
||||||
pendingScrollCompensationScheduled = true
|
|
||||||
const gen = scrollCompensationGen
|
|
||||||
|
|
||||||
// Flush in a microtask so compensation lands before the next paint.
|
|
||||||
queueMicrotask(() => {
|
|
||||||
if (gen !== scrollCompensationGen) return
|
|
||||||
pendingScrollCompensationScheduled = false
|
|
||||||
if (!containerRef) return
|
|
||||||
if (!hasUserScrollIntent() || lastUserScrollIntentDirection !== "up") {
|
|
||||||
pendingScrollCompensations = new Map()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (autoScroll() || anchorLock()) {
|
|
||||||
pendingScrollCompensations = new Map()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let applied = 0
|
|
||||||
let count = 0
|
|
||||||
for (const pendingDelta of pendingScrollCompensations.values()) {
|
|
||||||
if (!pendingDelta) continue
|
|
||||||
applied += pendingDelta
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
pendingScrollCompensations = new Map()
|
|
||||||
if (!applied) return
|
|
||||||
|
|
||||||
const before = containerRef.scrollTop
|
|
||||||
const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
|
|
||||||
const nextTop = Math.min(maxScrollTop, Math.max(0, before + applied))
|
|
||||||
if (nextTop !== before) {
|
|
||||||
containerRef.scrollTop = nextTop
|
|
||||||
lastKnownScrollTop = nextTop
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let pendingAutoPin = false
|
|
||||||
let pendingAutoPinFrame: number | null = null
|
|
||||||
|
|
||||||
function clearPendingAutoPinFrame() {
|
|
||||||
if (pendingAutoPinFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingAutoPinFrame)
|
|
||||||
pendingAutoPinFrame = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAutoPinToBottom() {
|
|
||||||
if (!containerRef) return false
|
|
||||||
if (!autoScroll()) return false
|
|
||||||
if (anchorLock()) return false
|
|
||||||
|
|
||||||
const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
|
|
||||||
if (containerRef.scrollTop !== maxScrollTop) {
|
|
||||||
containerRef.scrollTop = maxScrollTop
|
|
||||||
lastKnownScrollTop = maxScrollTop
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleAutoPinToBottom() {
|
|
||||||
if (!containerRef) return
|
|
||||||
if (pendingAutoPin) return
|
|
||||||
pendingAutoPin = true
|
|
||||||
clearPendingAutoPinFrame()
|
|
||||||
const gen = scrollCompensationGen
|
|
||||||
|
|
||||||
// Flush in a microtask so adjustments land before the next paint,
|
|
||||||
// then re-apply on the next two frames to catch deferred layout.
|
|
||||||
queueMicrotask(() => {
|
|
||||||
if (gen !== scrollCompensationGen) return
|
|
||||||
pendingAutoPin = false
|
|
||||||
if (!applyAutoPinToBottom()) return
|
|
||||||
pendingAutoPinFrame = requestAnimationFrame(() => {
|
|
||||||
pendingAutoPinFrame = null
|
|
||||||
if (gen !== scrollCompensationGen) return
|
|
||||||
if (!applyAutoPinToBottom()) return
|
|
||||||
pendingAutoPinFrame = requestAnimationFrame(() => {
|
|
||||||
pendingAutoPinFrame = null
|
|
||||||
if (gen !== scrollCompensationGen) return
|
|
||||||
applyAutoPinToBottom()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setShellRef(element: HTMLDivElement | null) {
|
|
||||||
shellRef = element || undefined
|
|
||||||
setShellElement(shellRef)
|
|
||||||
props.onShellElementChange?.(shellRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBottomSentinel(element: HTMLDivElement | null) {
|
|
||||||
setBottomSentinelSignal(element)
|
|
||||||
resolvePendingActiveScroll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const api: VirtualFollowListApi = {
|
const api: VirtualFollowListApi = {
|
||||||
scrollToTop: (opts) => scrollToTop(Boolean(opts?.immediate)),
|
scrollToTop: (opts) => scrollToTop(opts?.immediate ?? true),
|
||||||
scrollToBottom: (opts) => scrollToBottom(Boolean(opts?.immediate), { suppressAutoAnchor: opts?.suppressAutoAnchor }),
|
scrollToBottom: (opts) => scrollToBottom(opts?.immediate ?? true, { suppressAutoAnchor: opts?.suppressAutoAnchor }),
|
||||||
scrollToKey: (key, opts) => {
|
scrollToKey: (key, opts) => {
|
||||||
if (typeof document === "undefined") return
|
const index = props.items().findIndex((item, i) => props.getKey(item, i) === key)
|
||||||
const anchorId = getAnchorId(key)
|
if (index === -1) return
|
||||||
const behavior = opts?.behavior ?? "smooth"
|
|
||||||
const block = opts?.block ?? "start"
|
|
||||||
const nextAutoScroll = opts?.setAutoScroll ?? false
|
const nextAutoScroll = opts?.setAutoScroll ?? false
|
||||||
setAutoScroll(nextAutoScroll)
|
setAutoScroll(nextAutoScroll)
|
||||||
if (!nextAutoScroll) {
|
virtuaHandle()?.scrollToIndex(index, { align: opts?.block ?? "start", smooth: opts?.behavior === "smooth" })
|
||||||
if (anchorLock()) {
|
},
|
||||||
clearAnchorLock()
|
notifyContentRendered: () => {
|
||||||
}
|
if (autoScroll()) {
|
||||||
setAnchorLock({ key, block })
|
scrollToBottom(true)
|
||||||
} else {
|
}
|
||||||
if (anchorLock()) {
|
|
||||||
clearAnchorLock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const first = document.getElementById(anchorId)
|
|
||||||
first?.scrollIntoView({ block, behavior })
|
|
||||||
// When using virtualization, the placeholder height can be stale until the
|
|
||||||
// item mounts/measures. Re-run scrollIntoView() on the next frame to
|
|
||||||
// stabilize the final position.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const second = document.getElementById(anchorId)
|
|
||||||
second?.scrollIntoView({ block, behavior })
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
notifyContentRendered: () => handleContentRendered(),
|
|
||||||
setAutoScroll: (enabled) => setAutoScroll(Boolean(enabled)),
|
setAutoScroll: (enabled) => setAutoScroll(Boolean(enabled)),
|
||||||
getAutoScroll: () => autoScroll(),
|
getAutoScroll: () => autoScroll(),
|
||||||
getScrollElement: () => scrollElement(),
|
getScrollElement: () => scrollElement(),
|
||||||
getShellElement: () => shellElement(),
|
getShellElement: () => shellElement(),
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => props.registerApi?.(api))
|
||||||
props.registerApi?.(api)
|
createEffect(() => props.registerState?.(state))
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
// Handle autoScroll (Follow) on items change
|
||||||
props.registerState?.(state)
|
createEffect(on(() => props.items().length, (len, prevLen) => {
|
||||||
})
|
if (len > (prevLen ?? 0) && autoScroll() && !suppressAutoScrollOnce) {
|
||||||
|
requestAnimationFrame(() => scrollToBottom(true))
|
||||||
createEffect(() => {
|
|
||||||
const nextKey = props.resetKey?.()
|
|
||||||
if (nextKey === undefined) return
|
|
||||||
if (lastResetKey === undefined) {
|
|
||||||
lastResetKey = nextKey
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
suppressAutoScrollOnce = false
|
||||||
|
}, { defer: true }))
|
||||||
|
|
||||||
|
// Handle followToken change
|
||||||
|
createEffect(on(() => props.followToken?.(), () => {
|
||||||
|
if (autoScroll()) {
|
||||||
|
scrollToBottom(true)
|
||||||
|
}
|
||||||
|
}, { defer: true }))
|
||||||
|
|
||||||
|
// Reset state on resetKey change
|
||||||
|
createEffect(on(() => props.resetKey?.(), (nextKey) => {
|
||||||
if (nextKey === lastResetKey) return
|
if (nextKey === lastResetKey) return
|
||||||
lastResetKey = nextKey
|
lastResetKey = nextKey
|
||||||
|
setAutoScroll(initialAutoScroll())
|
||||||
// Reset internal state when consumers swap datasets (e.g. session switch).
|
|
||||||
if (pendingScrollFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingScrollFrame)
|
|
||||||
pendingScrollFrame = null
|
|
||||||
}
|
|
||||||
if (pendingAnchorScroll !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorScroll)
|
|
||||||
pendingAnchorScroll = null
|
|
||||||
}
|
|
||||||
if (pendingAnchorCorrectionFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorCorrectionFrame)
|
|
||||||
pendingAnchorCorrectionFrame = null
|
|
||||||
}
|
|
||||||
clearScrollToBottomFrames()
|
|
||||||
|
|
||||||
scrollCompensationGen += 1
|
|
||||||
pendingScrollCompensationScheduled = false
|
|
||||||
pendingScrollCompensations = new Map()
|
|
||||||
pendingAutoPin = false
|
|
||||||
clearPendingAutoPinFrame()
|
|
||||||
|
|
||||||
suppressAutoScrollOnce = false
|
|
||||||
pendingActiveScroll = false
|
|
||||||
pendingInitialScroll = true
|
pendingInitialScroll = true
|
||||||
|
}))
|
||||||
|
|
||||||
setAnchorLock(null)
|
// Initial scroll and session activation
|
||||||
setActiveKey(null)
|
|
||||||
setShowScrollTopButton(false)
|
|
||||||
setShowScrollBottomButton(false)
|
|
||||||
setTopSentinelVisible(true)
|
|
||||||
setBottomSentinelVisible(true)
|
|
||||||
setAutoScroll(Boolean(initialAutoScroll()))
|
|
||||||
|
|
||||||
lastKnownScrollTop = containerRef?.scrollTop ?? 0
|
|
||||||
lastUserScrollIntentDirection = null
|
|
||||||
})
|
|
||||||
|
|
||||||
let lastActiveState = false
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const active = isActive()
|
const active = isActive()
|
||||||
if (active) {
|
if (!active) return
|
||||||
resolvePendingActiveScroll()
|
if (pendingInitialScroll && props.items().length > 0) {
|
||||||
if (!lastActiveState && autoScroll() && scrollToBottomOnActivate()) {
|
pendingInitialScroll = false
|
||||||
requestScrollToBottom(true)
|
if (initialScrollToBottom()) {
|
||||||
|
scrollToBottom(true)
|
||||||
// When switching back to a cached session pane, items can mount/measure
|
|
||||||
// after the initial scroll jump. Re-pin once layout settles so the
|
|
||||||
// viewport stays at the bottom.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
scheduleAutoPinToBottom()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else if (autoScroll() && scrollToBottomOnActivate()) {
|
} else if (autoScroll() && scrollToBottomOnActivate()) {
|
||||||
pendingActiveScroll = true
|
scrollToBottom(true)
|
||||||
}
|
|
||||||
lastActiveState = active
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const loading = isLoading()
|
|
||||||
if (loading) {
|
|
||||||
// Keep the initial scroll pending while loading so we can
|
|
||||||
// anchor to the bottom as soon as items appear.
|
|
||||||
pendingInitialScroll = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pendingInitialScroll) return
|
|
||||||
|
|
||||||
const container = scrollElement()
|
|
||||||
const sentinel = bottomSentinel()
|
|
||||||
if (!container || !sentinel || props.items().length === 0) return
|
|
||||||
|
|
||||||
if (!initialScrollToBottom()) {
|
|
||||||
// An outer component is responsible for restoring scroll.
|
|
||||||
pendingInitialScroll = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we're in follow-to-bottom mode for the initial position.
|
|
||||||
if (anchorLock()) {
|
|
||||||
clearAnchorLock()
|
|
||||||
}
|
|
||||||
setAutoScroll(true)
|
|
||||||
|
|
||||||
pendingInitialScroll = false
|
|
||||||
// Scroll synchronously so the first paint prefers bottom content.
|
|
||||||
scrollToBottom(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
let previousFollowToken: string | number | undefined
|
|
||||||
createEffect(() => {
|
|
||||||
const token = props.followToken?.()
|
|
||||||
if (token === undefined) {
|
|
||||||
previousFollowToken = token
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (previousFollowToken === undefined) {
|
|
||||||
previousFollowToken = token
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (token === previousFollowToken) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
previousFollowToken = token
|
|
||||||
if (suppressAutoScrollOnce) {
|
|
||||||
suppressAutoScrollOnce = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (autoScroll()) {
|
|
||||||
scheduleAutoPinToBottom()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (anchorLock() && !autoScroll()) {
|
|
||||||
scheduleAnchorCorrection()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Drop anchor lock if the anchored key is removed.
|
return (
|
||||||
createEffect(() => {
|
<div class="virtual-follow-list-shell" ref={shellElement => {
|
||||||
const lock = anchorLock()
|
setShellElement(shellElement)
|
||||||
if (!lock) return
|
props.onShellElementChange?.(shellElement)
|
||||||
const keys = props.items().map((item, idx) => props.getKey(item, idx))
|
}}>
|
||||||
if (!keys.includes(lock.key)) {
|
<div
|
||||||
clearAnchorLock()
|
class="message-stream"
|
||||||
}
|
ref={el => {
|
||||||
})
|
setScrollElement(el)
|
||||||
|
props.onScrollElementChange?.(el)
|
||||||
|
attachScrollIntentListeners(el)
|
||||||
|
}}
|
||||||
|
onMouseUp={props.onMouseUp}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<Show when={props.renderBeforeItems}>
|
||||||
|
{props.renderBeforeItems!()}
|
||||||
|
</Show>
|
||||||
|
<Virtualizer
|
||||||
|
ref={setVirtuaHandle}
|
||||||
|
scrollRef={scrollElement()}
|
||||||
|
data={props.items()}
|
||||||
|
bufferSize={props.overscanPx ?? 400}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{(item, index) => props.renderItem(item, index())}
|
||||||
|
</Virtualizer>
|
||||||
|
</div>
|
||||||
|
|
||||||
createEffect(() => {
|
<Show when={props.renderOverlay}>
|
||||||
if (props.items().length === 0) {
|
<div class="virtual-follow-list-overlay">{props.renderOverlay!()}</div>
|
||||||
setShowScrollTopButton(false)
|
</Show>
|
||||||
setShowScrollBottomButton(false)
|
|
||||||
setAutoScroll(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateScrollIndicatorsFromVisibility()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
<Show when={props.renderControls}>
|
||||||
const container = scrollElement()
|
<div class="virtual-follow-list-controls-container">{props.renderControls!(state, api)}</div>
|
||||||
const topTarget = topSentinel()
|
</Show>
|
||||||
const bottomTarget = bottomSentinel()
|
|
||||||
if (!container || !topTarget || !bottomTarget) return
|
|
||||||
if (typeof IntersectionObserver === "undefined") return
|
|
||||||
|
|
||||||
const margin = props.scrollSentinelMarginPx ?? DEFAULT_SCROLL_SENTINEL_MARGIN_PX
|
<Show
|
||||||
|
when={
|
||||||
const observer = new IntersectionObserver(
|
!props.renderControls &&
|
||||||
(entries) => {
|
(showScrollTopButton() || showScrollBottomButton()) &&
|
||||||
let visibilityChanged = false
|
props.scrollToTopAriaLabel &&
|
||||||
for (const entry of entries) {
|
props.scrollToBottomAriaLabel
|
||||||
if (entry.target === topTarget) {
|
|
||||||
setTopSentinelVisible(entry.isIntersecting)
|
|
||||||
visibilityChanged = true
|
|
||||||
} else if (entry.target === bottomTarget) {
|
|
||||||
setBottomSentinelVisible(entry.isIntersecting)
|
|
||||||
visibilityChanged = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (visibilityChanged) {
|
>
|
||||||
updateScrollIndicatorsFromVisibility()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ root: container, threshold: 0, rootMargin: `${margin}px 0px ${margin}px 0px` },
|
|
||||||
)
|
|
||||||
observer.observe(topTarget)
|
|
||||||
observer.observe(bottomTarget)
|
|
||||||
onCleanup(() => observer.disconnect())
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const container = scrollElement()
|
|
||||||
const items = props.items()
|
|
||||||
if (!container || items.length === 0) return
|
|
||||||
if (typeof document === "undefined") return
|
|
||||||
if (typeof IntersectionObserver === "undefined") return
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
let best: IntersectionObserverEntry | null = null
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isIntersecting) continue
|
|
||||||
if (!best || entry.boundingClientRect.top < best.boundingClientRect.top) {
|
|
||||||
best = entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (best) {
|
|
||||||
const anchorId = (best.target as HTMLElement).id
|
|
||||||
const key = getKeyFromAnchorId(anchorId)
|
|
||||||
setActiveKey((current) => (current === key ? current : key))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ root: container, rootMargin: "-10% 0px -80% 0px", threshold: 0 },
|
|
||||||
)
|
|
||||||
|
|
||||||
const anchorIds = items.map((item, idx) => getAnchorId(props.getKey(item, idx)))
|
|
||||||
anchorIds.forEach((anchorId) => {
|
|
||||||
const anchor = document.getElementById(anchorId)
|
|
||||||
if (anchor) observer.observe(anchor)
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => observer.disconnect())
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const key = activeKey()
|
|
||||||
props.onActiveKeyChange?.(key)
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (pendingScrollFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingScrollFrame)
|
|
||||||
}
|
|
||||||
if (pendingAnchorScroll !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorScroll)
|
|
||||||
}
|
|
||||||
if (pendingAnchorCorrectionFrame !== null) {
|
|
||||||
cancelAnimationFrame(pendingAnchorCorrectionFrame)
|
|
||||||
}
|
|
||||||
scrollCompensationGen += 1
|
|
||||||
pendingScrollCompensationScheduled = false
|
|
||||||
pendingScrollCompensations = new Map()
|
|
||||||
clearPendingAutoPinFrame()
|
|
||||||
clearScrollToBottomFrames()
|
|
||||||
if (detachScrollIntentListeners) {
|
|
||||||
detachScrollIntentListeners()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const controls = () => {
|
|
||||||
if (props.renderControls) {
|
|
||||||
return props.renderControls(state, api)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid hardcoded user-visible strings; require consumers to supply
|
|
||||||
// localized aria labels when using the default controls.
|
|
||||||
if (!props.scrollToTopAriaLabel || !props.scrollToBottomAriaLabel) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelTop = props.scrollToTopAriaLabel()
|
|
||||||
const labelBottom = props.scrollToBottomAriaLabel()
|
|
||||||
return (
|
|
||||||
<Show when={showScrollTopButton() || showScrollBottomButton()}>
|
|
||||||
<div class="message-scroll-button-wrapper">
|
<div class="message-scroll-button-wrapper">
|
||||||
<Show when={showScrollTopButton()}>
|
<Show when={showScrollTopButton()}>
|
||||||
<button type="button" class="message-scroll-button" onClick={() => scrollToTop()} aria-label={labelTop}>
|
<button type="button" class="message-scroll-button" onClick={() => scrollToTop()} aria-label={props.scrollToTopAriaLabel!()}>
|
||||||
<span class="message-scroll-icon" aria-hidden="true">
|
<span class="message-scroll-icon" aria-hidden="true">
|
||||||
↑
|
↑
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={showScrollBottomButton()}>
|
<Show when={showScrollBottomButton()}>
|
||||||
<button
|
<button type="button" class="message-scroll-button" onClick={() => scrollToBottom()} aria-label={props.scrollToBottomAriaLabel!()}>
|
||||||
type="button"
|
|
||||||
class="message-scroll-button"
|
|
||||||
onClick={() => scrollToBottom(false, { suppressAutoAnchor: false })}
|
|
||||||
aria-label={labelBottom}
|
|
||||||
>
|
|
||||||
<span class="message-scroll-icon" aria-hidden="true">
|
<span class="message-scroll-icon" aria-hidden="true">
|
||||||
↓
|
↓
|
||||||
</span>
|
</span>
|
||||||
@@ -902,71 +393,6 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="message-stream-shell" ref={setShellRef}>
|
|
||||||
<div
|
|
||||||
class="message-stream"
|
|
||||||
ref={setContainerRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
onMouseUp={(event) => props.onMouseUp?.(event)}
|
|
||||||
onClick={(event) => props.onClick?.(event)}
|
|
||||||
>
|
|
||||||
<div ref={setTopSentinel} aria-hidden="true" style={{ height: "1px" }} />
|
|
||||||
{props.renderBeforeItems?.()}
|
|
||||||
<Index each={props.items()}>
|
|
||||||
{(item, index) => {
|
|
||||||
const key = () => props.getKey(item(), index)
|
|
||||||
const anchorId = () => getAnchorId(key())
|
|
||||||
const overscanPx = props.overscanPx ?? 800
|
|
||||||
const suspendMeasurements = () => measurementsSuspended() || !isActive()
|
|
||||||
const itemVirtualizationEnabled = () => virtualizationEnabled() && !autoScroll()
|
|
||||||
return (
|
|
||||||
<VirtualItem
|
|
||||||
id={anchorId()}
|
|
||||||
cacheKey={key()}
|
|
||||||
scrollContainer={scrollElement}
|
|
||||||
threshold={overscanPx}
|
|
||||||
placeholderClass="message-stream-placeholder"
|
|
||||||
virtualizationEnabled={itemVirtualizationEnabled}
|
|
||||||
suspendMeasurements={suspendMeasurements}
|
|
||||||
onHeightChange={(nextHeight, previousHeight, meta: VirtualItemHeightChangeMeta) => {
|
|
||||||
const delta = nextHeight - previousHeight
|
|
||||||
|
|
||||||
// Follow mode: keep the viewport pinned to the bottom as
|
|
||||||
// items mount/measure and change height.
|
|
||||||
if (delta && autoScroll() && !anchorLock()) {
|
|
||||||
scheduleAutoPinToBottom()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key-anchored mode: keep the target key in view when
|
|
||||||
// items above it mount/measure and shift layout.
|
|
||||||
if (anchorLock() && !autoScroll()) {
|
|
||||||
scheduleAnchorCorrection()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free-scroll mode: if items above the viewport change height
|
|
||||||
// while scrolling upward, compensate scrollTop so visible
|
|
||||||
// content stays stable.
|
|
||||||
if (delta) {
|
|
||||||
if (meta.isStaleCacheCorrection) return
|
|
||||||
scheduleScrollCompensation(key(), delta)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>{() => props.renderItem(item(), index)}</VirtualItem>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Index>
|
|
||||||
<div ref={setBottomSentinel} aria-hidden="true" style={{ height: "1px" }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{controls()}
|
|
||||||
|
|
||||||
{props.renderOverlay?.()}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,492 +0,0 @@
|
|||||||
import { JSX, Accessor, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
|
||||||
|
|
||||||
const sizeCache = new Map<string, number>()
|
|
||||||
const DEFAULT_MARGIN_PX = 600
|
|
||||||
const MIN_PLACEHOLDER_HEIGHT = 400
|
|
||||||
const VISIBILITY_BUFFER_PX = 0
|
|
||||||
|
|
||||||
type ObserverRoot = Element | Document | null
|
|
||||||
|
|
||||||
type IntersectionCallback = (entry: IntersectionObserverEntry) => void
|
|
||||||
|
|
||||||
interface SharedObserver {
|
|
||||||
observer: IntersectionObserver
|
|
||||||
listeners: Map<Element, Set<IntersectionCallback>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const NULL_ROOT_KEY = "__null__"
|
|
||||||
const rootIds = new WeakMap<Element | Document, number>()
|
|
||||||
let sharedRootId = 0
|
|
||||||
const sharedObservers = new Map<string, SharedObserver>()
|
|
||||||
|
|
||||||
function getRootKey(root: ObserverRoot, margin: number): string {
|
|
||||||
if (!root) {
|
|
||||||
return `${NULL_ROOT_KEY}:${margin}`
|
|
||||||
}
|
|
||||||
let id = rootIds.get(root)
|
|
||||||
if (id === undefined) {
|
|
||||||
id = ++sharedRootId
|
|
||||||
rootIds.set(root, id)
|
|
||||||
}
|
|
||||||
return `${id}:${margin}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSharedObserver(root: ObserverRoot, margin: number): SharedObserver {
|
|
||||||
const listeners = new Map<Element, Set<IntersectionCallback>>()
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const callbacks = listeners.get(entry.target as Element)
|
|
||||||
if (!callbacks) return
|
|
||||||
callbacks.forEach((fn) => fn(entry))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
root: root ?? undefined,
|
|
||||||
rootMargin: `${margin}px 0px ${margin}px 0px`,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return { observer, listeners }
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldRenderEntry(entry: IntersectionObserverEntry) {
|
|
||||||
const rootBounds = entry.rootBounds
|
|
||||||
if (!rootBounds) {
|
|
||||||
return entry.isIntersecting
|
|
||||||
}
|
|
||||||
|
|
||||||
// Above the root: compare bottom edge to root top.
|
|
||||||
if (entry.boundingClientRect.bottom < rootBounds.top) {
|
|
||||||
const distance = rootBounds.top - entry.boundingClientRect.bottom
|
|
||||||
return distance <= VISIBILITY_BUFFER_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below the root: compare top edge to root bottom.
|
|
||||||
if (entry.boundingClientRect.top > rootBounds.bottom) {
|
|
||||||
const distance = entry.boundingClientRect.top - rootBounds.bottom
|
|
||||||
return distance <= VISIBILITY_BUFFER_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlapping the root bounds.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getViewportRect(): { top: number; bottom: number } {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return { top: 0, bottom: 0 }
|
|
||||||
}
|
|
||||||
return { top: 0, bottom: window.innerHeight }
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRenderableRoot(root: ObserverRoot): boolean {
|
|
||||||
if (!root) return true
|
|
||||||
if (root instanceof Document) return true
|
|
||||||
if (typeof window === "undefined") return false
|
|
||||||
|
|
||||||
const element = root as Element
|
|
||||||
const style = window.getComputedStyle(element as Element)
|
|
||||||
if (style.display === "none" || style.visibility === "hidden") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const rect = (element as Element).getBoundingClientRect()
|
|
||||||
return rect.width > 0 && rect.height > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldRenderByRects(params: {
|
|
||||||
wrapperRect: DOMRect
|
|
||||||
rootRect: { top: number; bottom: number }
|
|
||||||
margin: number
|
|
||||||
}): boolean {
|
|
||||||
const { wrapperRect, rootRect, margin } = params
|
|
||||||
const threshold = margin + VISIBILITY_BUFFER_PX
|
|
||||||
|
|
||||||
// Above the root: compare bottom edge to root top.
|
|
||||||
if (wrapperRect.bottom < rootRect.top) {
|
|
||||||
const distance = rootRect.top - wrapperRect.bottom
|
|
||||||
return distance <= threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below the root: compare top edge to root bottom.
|
|
||||||
if (wrapperRect.top > rootRect.bottom) {
|
|
||||||
const distance = wrapperRect.top - rootRect.bottom
|
|
||||||
return distance <= threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeToSharedObserver(
|
|
||||||
target: Element,
|
|
||||||
root: ObserverRoot,
|
|
||||||
margin: number,
|
|
||||||
callback: IntersectionCallback,
|
|
||||||
): () => void {
|
|
||||||
if (typeof IntersectionObserver === "undefined") {
|
|
||||||
callback({ isIntersecting: true } as IntersectionObserverEntry)
|
|
||||||
return () => {}
|
|
||||||
}
|
|
||||||
const key = getRootKey(root, margin)
|
|
||||||
let shared = sharedObservers.get(key)
|
|
||||||
if (!shared) {
|
|
||||||
shared = createSharedObserver(root, margin)
|
|
||||||
sharedObservers.set(key, shared)
|
|
||||||
}
|
|
||||||
let targetCallbacks = shared.listeners.get(target)
|
|
||||||
if (!targetCallbacks) {
|
|
||||||
targetCallbacks = new Set()
|
|
||||||
shared.listeners.set(target, targetCallbacks)
|
|
||||||
shared.observer.observe(target)
|
|
||||||
}
|
|
||||||
targetCallbacks.add(callback)
|
|
||||||
return () => {
|
|
||||||
const current = shared?.listeners.get(target)
|
|
||||||
if (current) {
|
|
||||||
current.delete(callback)
|
|
||||||
if (current.size === 0) {
|
|
||||||
shared?.listeners.delete(target)
|
|
||||||
shared?.observer.unobserve(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shared && shared.listeners.size === 0) {
|
|
||||||
shared.observer.disconnect()
|
|
||||||
sharedObservers.delete(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VirtualItemProps {
|
|
||||||
cacheKey: string
|
|
||||||
children: JSX.Element | (() => JSX.Element)
|
|
||||||
scrollContainer?: Accessor<HTMLElement | undefined | null>
|
|
||||||
threshold?: number
|
|
||||||
minPlaceholderHeight?: number
|
|
||||||
class?: string
|
|
||||||
contentClass?: string
|
|
||||||
placeholderClass?: string
|
|
||||||
virtualizationEnabled?: Accessor<boolean>
|
|
||||||
forceVisible?: Accessor<boolean>
|
|
||||||
suspendMeasurements?: Accessor<boolean>
|
|
||||||
onMeasured?: () => void
|
|
||||||
onHeightChange?: (nextHeight: number, previousHeight: number, meta: VirtualItemHeightChangeMeta) => void
|
|
||||||
id?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VirtualItemHeightChangeMeta {
|
|
||||||
source: "initial-visible-measure" | "resize"
|
|
||||||
previousCachedHeight: number | null
|
|
||||||
isStaleCacheCorrection: boolean
|
|
||||||
wasHidden: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function VirtualItem(props: VirtualItemProps) {
|
|
||||||
const resolveContent = () => (typeof props.children === "function" ? (props.children as () => JSX.Element)() : props.children)
|
|
||||||
const cachedHeight = sizeCache.get(props.cacheKey)
|
|
||||||
const fallbackPlaceholderHeight = () => props.minPlaceholderHeight ?? MIN_PLACEHOLDER_HEIGHT
|
|
||||||
// Default to hidden until we can determine visibility.
|
|
||||||
// This avoids keeping heavy DOM alive when IntersectionObserver
|
|
||||||
// doesn't fire (common for hidden/zero-sized scroll roots).
|
|
||||||
const [isIntersecting, setIsIntersecting] = createSignal(false)
|
|
||||||
// Keep measuredHeight aligned with the *effective layout height* while hidden.
|
|
||||||
// When content first mounts, onHeightChange deltas should reflect the DOM's
|
|
||||||
// placeholder height (not 0), otherwise scroll compensation can overshoot.
|
|
||||||
const [measuredHeight, setMeasuredHeight] = createSignal(cachedHeight ?? fallbackPlaceholderHeight())
|
|
||||||
let hasReportedMeasurement = Boolean(cachedHeight && cachedHeight > 0)
|
|
||||||
let pendingVisibility: boolean | null = null
|
|
||||||
let visibilityFrame: number | null = null
|
|
||||||
let awaitingVisibleMeasurement = true
|
|
||||||
let lastMeasurementWhileHidden = true
|
|
||||||
const flushVisibility = () => {
|
|
||||||
if (visibilityFrame !== null) {
|
|
||||||
cancelAnimationFrame(visibilityFrame)
|
|
||||||
visibilityFrame = null
|
|
||||||
}
|
|
||||||
if (pendingVisibility !== null) {
|
|
||||||
setIsIntersecting(pendingVisibility)
|
|
||||||
pendingVisibility = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const queueVisibility = (nextValue: boolean) => {
|
|
||||||
pendingVisibility = nextValue
|
|
||||||
if (visibilityFrame !== null) return
|
|
||||||
visibilityFrame = requestAnimationFrame(() => {
|
|
||||||
visibilityFrame = null
|
|
||||||
if (pendingVisibility !== null) {
|
|
||||||
setIsIntersecting(pendingVisibility)
|
|
||||||
pendingVisibility = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
|
|
||||||
const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())
|
|
||||||
const forceVisible = () => Boolean(props.forceVisible?.())
|
|
||||||
const shouldHideContent = createMemo(() => {
|
|
||||||
if (forceVisible()) return false
|
|
||||||
if (!virtualizationEnabled()) return false
|
|
||||||
return !isIntersecting()
|
|
||||||
})
|
|
||||||
|
|
||||||
let wrapperRef: HTMLDivElement | undefined
|
|
||||||
let contentRef: HTMLDivElement | undefined
|
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver | undefined
|
|
||||||
let intersectionCleanup: (() => void) | undefined
|
|
||||||
|
|
||||||
function cleanupResizeObserver() {
|
|
||||||
if (resizeObserver) {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
resizeObserver = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleVisibleMeasurements() {
|
|
||||||
if (shouldHideContent() || measurementsSuspended()) return
|
|
||||||
if (!contentRef) return
|
|
||||||
queueMicrotask(() => {
|
|
||||||
if (shouldHideContent() || measurementsSuspended()) return
|
|
||||||
if (!contentRef) return
|
|
||||||
updateMeasuredHeight()
|
|
||||||
setupResizeObserver()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupIntersectionObserver() {
|
|
||||||
if (intersectionCleanup) {
|
|
||||||
intersectionCleanup()
|
|
||||||
intersectionCleanup = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistMeasurement(nextHeight: number, meta?: { source: "initial-visible-measure" | "resize"; wasHidden: boolean }) {
|
|
||||||
if (!Number.isFinite(nextHeight) || nextHeight < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const before = measuredHeight()
|
|
||||||
const normalized = nextHeight
|
|
||||||
const previousCachedHeight = sizeCache.get(props.cacheKey) ?? null
|
|
||||||
const previous = previousCachedHeight ?? measuredHeight()
|
|
||||||
const measurementMeta: VirtualItemHeightChangeMeta = {
|
|
||||||
source: meta?.source ?? "resize",
|
|
||||||
previousCachedHeight,
|
|
||||||
isStaleCacheCorrection:
|
|
||||||
(meta?.source ?? "resize") === "initial-visible-measure" &&
|
|
||||||
previousCachedHeight !== null &&
|
|
||||||
normalized > 0 &&
|
|
||||||
Math.abs(normalized - previousCachedHeight) > 1,
|
|
||||||
wasHidden: meta?.wasHidden ?? shouldHideContent(),
|
|
||||||
}
|
|
||||||
// Only keep the previous measurement when the element reports 0 height.
|
|
||||||
// Allow shrinkage so placeholder height matches real content height;
|
|
||||||
// keeping the max height can cause mount/unmount jitter near the
|
|
||||||
// virtualization boundary.
|
|
||||||
const shouldKeepPrevious = previous > 0 && normalized === 0
|
|
||||||
if (shouldKeepPrevious) {
|
|
||||||
if (!hasReportedMeasurement) {
|
|
||||||
hasReportedMeasurement = true
|
|
||||||
props.onMeasured?.()
|
|
||||||
}
|
|
||||||
sizeCache.set(props.cacheKey, previous)
|
|
||||||
setMeasuredHeight(previous)
|
|
||||||
if (previous !== before) props.onHeightChange?.(previous, before, measurementMeta)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (normalized > 0) {
|
|
||||||
sizeCache.set(props.cacheKey, normalized)
|
|
||||||
if (!hasReportedMeasurement) {
|
|
||||||
hasReportedMeasurement = true
|
|
||||||
props.onMeasured?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setMeasuredHeight(normalized)
|
|
||||||
if (normalized !== before) props.onHeightChange?.(normalized, before, measurementMeta)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMeasuredHeight() {
|
|
||||||
if (!contentRef) return
|
|
||||||
if (measurementsSuspended()) return
|
|
||||||
// Prefer subpixel-accurate height for scroll compensation.
|
|
||||||
// offsetHeight rounds to integers which can accumulate error.
|
|
||||||
const rect = contentRef.getBoundingClientRect()
|
|
||||||
const next = Math.max(0, Math.round(rect.height * 2) / 2)
|
|
||||||
const currentMeasured = measuredHeight()
|
|
||||||
const measurementSource: "initial-visible-measure" | "resize" = awaitingVisibleMeasurement ? "initial-visible-measure" : "resize"
|
|
||||||
const wasHidden = lastMeasurementWhileHidden
|
|
||||||
if (measurementSource === "initial-visible-measure") {
|
|
||||||
awaitingVisibleMeasurement = false
|
|
||||||
lastMeasurementWhileHidden = false
|
|
||||||
}
|
|
||||||
if (next === currentMeasured) return
|
|
||||||
persistMeasurement(next, { source: measurementSource, wasHidden })
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupResizeObserver() {
|
|
||||||
if (!contentRef || measurementsSuspended()) return
|
|
||||||
cleanupResizeObserver()
|
|
||||||
if (typeof ResizeObserver === "undefined") {
|
|
||||||
updateMeasuredHeight()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
if (measurementsSuspended()) return
|
|
||||||
updateMeasuredHeight()
|
|
||||||
})
|
|
||||||
resizeObserver.observe(contentRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function refreshIntersectionObserver(targetRoot: Element | Document | null) {
|
|
||||||
cleanupIntersectionObserver()
|
|
||||||
if (!wrapperRef) {
|
|
||||||
setIsIntersecting(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (typeof IntersectionObserver === "undefined") {
|
|
||||||
setIsIntersecting(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const margin = props.threshold ?? DEFAULT_MARGIN_PX
|
|
||||||
|
|
||||||
// If the scroll root is hidden / 0x0, IntersectionObserver can report
|
|
||||||
// `isIntersecting` in unexpected ways (often "true" with null rootBounds),
|
|
||||||
// which keeps heavy DOM alive in background tabs.
|
|
||||||
//
|
|
||||||
// In that state, force-hide and skip attaching the observer. When the
|
|
||||||
// pane becomes visible again, VirtualItem will re-run this setup and
|
|
||||||
// re-attach the observer.
|
|
||||||
const renderable = isRenderableRoot(targetRoot)
|
|
||||||
if (!renderable) {
|
|
||||||
setIsIntersecting(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid doing an eager geometry read here.
|
|
||||||
// During large list hydration / initial layout, wrapper rects can be
|
|
||||||
// transiently 0/incorrect and cause many offscreen items to mount.
|
|
||||||
// Rely on the observer callback (which we harden below) to determine
|
|
||||||
// visibility.
|
|
||||||
|
|
||||||
const wrapperEl = wrapperRef
|
|
||||||
intersectionCleanup = subscribeToSharedObserver(wrapperEl, targetRoot, margin, (entry) => {
|
|
||||||
// IntersectionObserver can produce transient false-positives during pane
|
|
||||||
// activation/layout transitions (e.g. `isIntersecting: true` for items far
|
|
||||||
// outside the scroll root). For element roots, prefer explicit rect math.
|
|
||||||
if (targetRoot && !(targetRoot instanceof Document)) {
|
|
||||||
// When rootBounds is null we cannot trust the entry; treat as hidden.
|
|
||||||
if (entry.rootBounds === null) {
|
|
||||||
queueVisibility(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const rootRect = (targetRoot as Element).getBoundingClientRect()
|
|
||||||
const visible = shouldRenderByRects({
|
|
||||||
wrapperRect: wrapperEl.getBoundingClientRect(),
|
|
||||||
rootRect: { top: rootRect.top, bottom: rootRect.bottom },
|
|
||||||
margin,
|
|
||||||
})
|
|
||||||
queueVisibility(visible)
|
|
||||||
return
|
|
||||||
} catch {
|
|
||||||
// Fall through to the entry-based heuristic.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextVisible = shouldRenderEntry(entry)
|
|
||||||
queueVisibility(nextVisible)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWrapperRef(element: HTMLDivElement | null) {
|
|
||||||
wrapperRef = element ?? undefined
|
|
||||||
const root = props.scrollContainer ? props.scrollContainer() : null
|
|
||||||
refreshIntersectionObserver(root ?? null)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setContentRef(element: HTMLDivElement | null) {
|
|
||||||
contentRef = element ?? undefined
|
|
||||||
if (contentRef) {
|
|
||||||
queueMicrotask(() => {
|
|
||||||
if (shouldHideContent() || measurementsSuspended()) return
|
|
||||||
updateMeasuredHeight()
|
|
||||||
setupResizeObserver()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
cleanupResizeObserver()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createEffect(() => {
|
|
||||||
const hidden = shouldHideContent()
|
|
||||||
if (hidden) {
|
|
||||||
awaitingVisibleMeasurement = true
|
|
||||||
lastMeasurementWhileHidden = true
|
|
||||||
}
|
|
||||||
if (hidden || measurementsSuspended()) {
|
|
||||||
cleanupResizeObserver()
|
|
||||||
}
|
|
||||||
if (!hidden && !measurementsSuspended() && contentRef) {
|
|
||||||
scheduleVisibleMeasurements()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const key = props.cacheKey
|
|
||||||
|
|
||||||
const cached = sizeCache.get(key)
|
|
||||||
if (cached !== undefined) {
|
|
||||||
setMeasuredHeight(cached)
|
|
||||||
} else {
|
|
||||||
setMeasuredHeight(fallbackPlaceholderHeight())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
measurementsSuspended()
|
|
||||||
const root = props.scrollContainer ? props.scrollContainer() : null
|
|
||||||
refreshIntersectionObserver(root ?? null)
|
|
||||||
})
|
|
||||||
|
|
||||||
const placeholderHeight = createMemo(() => {
|
|
||||||
|
|
||||||
const seenHeight = measuredHeight()
|
|
||||||
if (seenHeight > 0) {
|
|
||||||
return seenHeight
|
|
||||||
}
|
|
||||||
return props.minPlaceholderHeight ?? MIN_PLACEHOLDER_HEIGHT
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
cleanupResizeObserver()
|
|
||||||
cleanupIntersectionObserver()
|
|
||||||
flushVisibility()
|
|
||||||
})
|
|
||||||
|
|
||||||
const wrapperClass = () => ["virtual-item-wrapper", props.class].filter(Boolean).join(" ")
|
|
||||||
const contentClass = () => {
|
|
||||||
const classes = ["virtual-item-content", props.contentClass]
|
|
||||||
if (shouldHideContent()) {
|
|
||||||
classes.push("virtual-item-content-hidden")
|
|
||||||
}
|
|
||||||
return classes.filter(Boolean).join(" ")
|
|
||||||
}
|
|
||||||
const placeholderClass = () => ["virtual-item-placeholder", props.placeholderClass].filter(Boolean).join(" ")
|
|
||||||
const lazyContent = createMemo<JSX.Element | null>(() => {
|
|
||||||
if (shouldHideContent()) return null
|
|
||||||
return resolveContent()
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={setWrapperRef} id={props.id} class={wrapperClass()} style={{ width: "100%" }}>
|
|
||||||
<div
|
|
||||||
class={placeholderClass()}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: shouldHideContent() ? `${placeholderHeight()}px` : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div ref={setContentRef} class={contentClass()}>
|
|
||||||
{lazyContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
setWorktreeSlugForParentSession,
|
setWorktreeSlugForParentSession,
|
||||||
} from "../stores/worktrees"
|
} from "../stores/worktrees"
|
||||||
import { sessions } from "../stores/sessions"
|
import { sessions } from "../stores/sessions"
|
||||||
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
@@ -25,8 +26,6 @@ type WorktreeOption =
|
|||||||
| { kind: "action"; key: "__create__"; label: string }
|
| { kind: "action"; key: "__create__"; label: string }
|
||||||
| { kind: "worktree"; key: string; slug: string; directory: string; raw: WorktreeDescriptor }
|
| { kind: "worktree"; key: string; slug: string; directory: string; raw: WorktreeDescriptor }
|
||||||
|
|
||||||
const CREATE_OPTION: WorktreeOption = { kind: "action", key: "__create__", label: "+ Create worktree" }
|
|
||||||
|
|
||||||
function preventSelectPress(event: PointerEvent | MouseEvent) {
|
function preventSelectPress(event: PointerEvent | MouseEvent) {
|
||||||
// Prevent Select.Item from treating this as a selection.
|
// Prevent Select.Item from treating this as a selection.
|
||||||
// We intentionally prevent default to stop Kobalte's internal press handling.
|
// We intentionally prevent default to stop Kobalte's internal press handling.
|
||||||
@@ -71,6 +70,7 @@ interface WorktreeSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
const [isOpen, setIsOpen] = createSignal(false)
|
||||||
const [createOpen, setCreateOpen] = createSignal(false)
|
const [createOpen, setCreateOpen] = createSignal(false)
|
||||||
const [createSlug, setCreateSlug] = createSignal("")
|
const [createSlug, setCreateSlug] = createSignal("")
|
||||||
@@ -99,7 +99,8 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
|||||||
directory: wt.directory,
|
directory: wt.directory,
|
||||||
raw: wt,
|
raw: wt,
|
||||||
}))
|
}))
|
||||||
return [CREATE_OPTION, ...mapped]
|
const createOption: WorktreeOption = { kind: "action", key: "__create__", label: t("instanceShell.worktree.create") }
|
||||||
|
return [createOption, ...mapped]
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedOption = createMemo<WorktreeOption | undefined>(() => {
|
const selectedOption = createMemo<WorktreeOption | undefined>(() => {
|
||||||
|
|||||||
23
packages/ui/src/lib/external-url.ts
Normal file
23
packages/ui/src/lib/external-url.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { isTauriHost } from "./runtime-env"
|
||||||
|
|
||||||
|
export async function openExternalUrl(url: string, context = "ui"): Promise<void> {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTauriHost()) {
|
||||||
|
try {
|
||||||
|
const { openUrl } = await import("@tauri-apps/plugin-opener")
|
||||||
|
await openUrl(url)
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[${context}] unable to open via system opener`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer")
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[${context}] unable to open external url`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,27 +2,32 @@ import { createContext, createEffect, createMemo, createSignal, onCleanup, onMou
|
|||||||
import type { ParentComponent } from "solid-js"
|
import type { ParentComponent } from "solid-js"
|
||||||
import { useConfig } from "../../stores/preferences"
|
import { useConfig } from "../../stores/preferences"
|
||||||
import { enMessages } from "./messages/en"
|
import { enMessages } from "./messages/en"
|
||||||
import { esMessages } from "./messages/es"
|
|
||||||
import { frMessages } from "./messages/fr"
|
|
||||||
import { ruMessages } from "./messages/ru"
|
|
||||||
import { jaMessages } from "./messages/ja"
|
|
||||||
import { zhHansMessages } from "./messages/zh-Hans"
|
|
||||||
|
|
||||||
type Messages = Record<string, string>
|
type Messages = Record<string, string>
|
||||||
|
|
||||||
export type TranslateParams = Record<string, unknown>
|
export type TranslateParams = Record<string, unknown>
|
||||||
|
|
||||||
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans" | "he"
|
||||||
|
|
||||||
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans", "he"] as const
|
||||||
|
const SUPPORTED_LOCALES_BY_LOWER = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
||||||
|
const RTL_LOCALES = new Set<Locale>(["he"])
|
||||||
|
|
||||||
const messagesByLocale: Record<Locale, Messages> = {
|
const localeMessagesCache = new Map<Locale, Messages>([["en", enMessages]])
|
||||||
en: enMessages,
|
const localeMessagesPromises = new Map<Locale, Promise<Messages>>()
|
||||||
es: esMessages,
|
|
||||||
fr: frMessages,
|
const localeLoaders: Record<Locale, () => Promise<Messages>> = {
|
||||||
ru: ruMessages,
|
en: async () => enMessages,
|
||||||
ja: jaMessages,
|
es: async () => (await import("./messages/es")).esMessages,
|
||||||
"zh-Hans": zhHansMessages,
|
fr: async () => (await import("./messages/fr")).frMessages,
|
||||||
|
ru: async () => (await import("./messages/ru")).ruMessages,
|
||||||
|
ja: async () => (await import("./messages/ja")).jaMessages,
|
||||||
|
"zh-Hans": async () => (await import("./messages/zh-Hans")).zhHansMessages,
|
||||||
|
he: async () => (await import("./messages/he")).heMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocaleDirection(locale: Locale): "ltr" | "rtl" {
|
||||||
|
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLocaleTag(value: string): string {
|
function normalizeLocaleTag(value: string): string {
|
||||||
@@ -34,8 +39,7 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
|||||||
|
|
||||||
const normalized = normalizeLocaleTag(value)
|
const normalized = normalizeLocaleTag(value)
|
||||||
const lower = normalized.toLowerCase()
|
const lower = normalized.toLowerCase()
|
||||||
const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
const exact = SUPPORTED_LOCALES_BY_LOWER.get(lower)
|
||||||
const exact = supportedLower.get(lower)
|
|
||||||
if (exact) return exact
|
if (exact) return exact
|
||||||
|
|
||||||
const parts = lower.split("-")
|
const parts = lower.split("-")
|
||||||
@@ -43,11 +47,11 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
|||||||
if (!base) return null
|
if (!base) return null
|
||||||
|
|
||||||
if (base === "zh") {
|
if (base === "zh") {
|
||||||
const zhHans = supportedLower.get("zh-hans")
|
const zhHans = SUPPORTED_LOCALES_BY_LOWER.get("zh-hans")
|
||||||
return zhHans ?? null
|
return zhHans ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseMatch = supportedLower.get(base)
|
const baseMatch = SUPPORTED_LOCALES_BY_LOWER.get(base)
|
||||||
return baseMatch ?? null
|
return baseMatch ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +88,54 @@ function translateFrom(messages: Messages, key: string, params?: TranslateParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [globalRevision, setGlobalRevision] = createSignal(0)
|
const [globalRevision, setGlobalRevision] = createSignal(0)
|
||||||
const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en"
|
let globalMessages: Messages = enMessages
|
||||||
let globalMessages: Messages = messagesByLocale[initialGlobalLocale]
|
let globalLocale: Locale = "en"
|
||||||
|
|
||||||
|
function getMessagesForLocale(locale: Locale): Messages {
|
||||||
|
return localeMessagesCache.get(locale) ?? enMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLocaleMessages(locale: Locale): Promise<Messages> {
|
||||||
|
const cached = localeMessagesCache.get(locale)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = localeMessagesPromises.get(locale)
|
||||||
|
if (pending) {
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = localeLoaders[locale]
|
||||||
|
const promise = loader()
|
||||||
|
.then((messages) => {
|
||||||
|
localeMessagesCache.set(locale, messages)
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
localeMessagesPromises.set(locale, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadLocaleMessages(preferredLocale?: string | null): Promise<Locale> {
|
||||||
|
const resolvedLocale = matchSupportedLocale(preferredLocale ?? undefined) ?? detectNavigatorLocale() ?? "en"
|
||||||
|
try {
|
||||||
|
globalMessages = await loadLocaleMessages(resolvedLocale)
|
||||||
|
globalLocale = resolvedLocale
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return resolvedLocale
|
||||||
|
} catch {
|
||||||
|
globalMessages = enMessages
|
||||||
|
globalLocale = "en"
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function tGlobal(key: string, params?: TranslateParams): string {
|
export function tGlobal(key: string, params?: TranslateParams): string {
|
||||||
globalRevision()
|
globalRevision()
|
||||||
@@ -101,9 +151,12 @@ const I18nContext = createContext<I18nContextValue>()
|
|||||||
|
|
||||||
export const I18nProvider: ParentComponent = (props) => {
|
export const I18nProvider: ParentComponent = (props) => {
|
||||||
const { preferences } = useConfig()
|
const { preferences } = useConfig()
|
||||||
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
|
const [detectedLocale, setDetectedLocale] = createSignal<Locale>(globalLocale)
|
||||||
|
const [resolvedLocale, setResolvedLocale] = createSignal<Locale>(globalLocale)
|
||||||
const previousMessages = globalMessages
|
const previousGlobalMessages = globalMessages
|
||||||
|
const previousGlobalLocale = globalLocale
|
||||||
|
const previousDocumentLanguage = typeof document !== "undefined" ? document.documentElement.lang : ""
|
||||||
|
const previousDocumentDirection = typeof document !== "undefined" ? document.documentElement.dir : ""
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const detected = detectNavigatorLocale()
|
const detected = detectNavigatorLocale()
|
||||||
@@ -115,20 +168,56 @@ export const I18nProvider: ParentComponent = (props) => {
|
|||||||
return configured ?? detectedLocale() ?? "en"
|
return configured ?? detectedLocale() ?? "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = createMemo<Messages>(() => messagesByLocale[locale()])
|
const messages = createMemo<Messages>(() => getMessagesForLocale(resolvedLocale()))
|
||||||
|
|
||||||
function t(key: string, params?: TranslateParams): string {
|
function t(key: string, params?: TranslateParams): string {
|
||||||
return translateFrom(messages(), key, params)
|
return translateFrom(messages(), key, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
globalMessages = messages()
|
const nextLocale = locale()
|
||||||
setGlobalRevision((value) => value + 1)
|
let cancelled = false
|
||||||
|
|
||||||
|
void loadLocaleMessages(nextLocale)
|
||||||
|
.then((loadedMessages) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale(nextLocale)
|
||||||
|
globalLocale = nextLocale
|
||||||
|
globalMessages = loadedMessages
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale("en")
|
||||||
|
globalMessages = enMessages
|
||||||
|
globalLocale = "en"
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
cancelled = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
const activeLocale = locale()
|
||||||
|
document.documentElement.dir = getLocaleDirection(activeLocale)
|
||||||
|
document.documentElement.lang = activeLocale
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
globalMessages = previousMessages
|
globalMessages = previousGlobalMessages
|
||||||
|
globalLocale = previousGlobalLocale
|
||||||
setGlobalRevision((value) => value + 1)
|
setGlobalRevision((value) => value + 1)
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.lang = previousDocumentLanguage
|
||||||
|
document.documentElement.dir = previousDocumentDirection
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const value: I18nContextValue = {
|
const value: I18nContextValue = {
|
||||||
|
|||||||
@@ -114,12 +114,26 @@ export const instanceMessages = {
|
|||||||
"instanceShell.sessionChanges.filesChanged": "{count} files changed",
|
"instanceShell.sessionChanges.filesChanged": "{count} files changed",
|
||||||
"instanceShell.sessionChanges.actions.show": "Show changes",
|
"instanceShell.sessionChanges.actions.show": "Show changes",
|
||||||
|
|
||||||
|
"instanceShell.gitChanges.noSessionSelected": "Select a session to view git changes.",
|
||||||
|
"instanceShell.gitChanges.loading": "Loading git changes...",
|
||||||
|
"instanceShell.gitChanges.empty": "No git changes yet.",
|
||||||
|
"instanceShell.gitChanges.deleted": "Deleted",
|
||||||
|
|
||||||
"instanceShell.filesShell.fileListTitle": "File list",
|
"instanceShell.filesShell.fileListTitle": "File list",
|
||||||
"instanceShell.filesShell.mobileSelectorLabel": "Select file",
|
"instanceShell.filesShell.mobileSelectorLabel": "Select file",
|
||||||
"instanceShell.filesShell.mobileSelectorEmpty": "Select a file",
|
"instanceShell.filesShell.mobileSelectorEmpty": "Select a file",
|
||||||
"instanceShell.filesShell.viewerTitle": "Change viewer",
|
"instanceShell.filesShell.viewerTitle": "Change viewer",
|
||||||
"instanceShell.filesShell.viewerPlaceholder": "Detailed change rendering will be added in the next step.",
|
"instanceShell.filesShell.viewerPlaceholder": "Detailed change rendering will be added in the next step.",
|
||||||
"instanceShell.filesShell.viewerEmpty": "No file selected.",
|
"instanceShell.filesShell.viewerEmpty": "No file selected.",
|
||||||
|
"instanceShell.filesShell.hideFiles": "Hide files",
|
||||||
|
"instanceShell.filesShell.showFiles": "Show files",
|
||||||
|
"instanceShell.diff.hideUnchanged": "Hide unchanged regions",
|
||||||
|
"instanceShell.diff.showFull": "Show full file",
|
||||||
|
"instanceShell.diff.switchToSplit": "Switch to split view",
|
||||||
|
"instanceShell.diff.switchToUnified": "Switch to unified view",
|
||||||
|
"instanceShell.diff.enableWordWrap": "Enable word wrap",
|
||||||
|
"instanceShell.diff.disableWordWrap": "Disable word wrap",
|
||||||
|
"instanceShell.worktree.create": "+ Create worktree",
|
||||||
|
|
||||||
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
||||||
"instanceShell.plan.empty": "Nothing planned yet.",
|
"instanceShell.plan.empty": "Nothing planned yet.",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.rightPanel.title": "Panel de estado",
|
"instanceShell.rightPanel.title": "Panel de estado",
|
||||||
"instanceShell.rightPanel.tabs.changes": "Cambios",
|
"instanceShell.rightPanel.tabs.changes": "Cambios",
|
||||||
|
"instanceShell.rightPanel.tabs.gitChanges": "Cambios de Git",
|
||||||
"instanceShell.rightPanel.tabs.files": "Archivos",
|
"instanceShell.rightPanel.tabs.files": "Archivos",
|
||||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||||
@@ -112,6 +113,10 @@ export const instanceMessages = {
|
|||||||
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
||||||
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
||||||
|
|
||||||
|
"instanceShell.gitChanges.loading": "Cargando cambios de Git...",
|
||||||
|
"instanceShell.gitChanges.empty": "Aún no hay cambios de Git.",
|
||||||
|
"instanceShell.gitChanges.deleted": "Eliminado",
|
||||||
|
|
||||||
"instanceShell.filesShell.fileListTitle": "Lista de archivos",
|
"instanceShell.filesShell.fileListTitle": "Lista de archivos",
|
||||||
"instanceShell.filesShell.mobileSelectorLabel": "Seleccionar archivo",
|
"instanceShell.filesShell.mobileSelectorLabel": "Seleccionar archivo",
|
||||||
"instanceShell.filesShell.mobileSelectorEmpty": "Selecciona un archivo",
|
"instanceShell.filesShell.mobileSelectorEmpty": "Selecciona un archivo",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.rightPanel.title": "Panneau d'état",
|
"instanceShell.rightPanel.title": "Panneau d'état",
|
||||||
"instanceShell.rightPanel.tabs.changes": "Modifications",
|
"instanceShell.rightPanel.tabs.changes": "Modifications",
|
||||||
|
"instanceShell.rightPanel.tabs.gitChanges": "Changements Git",
|
||||||
"instanceShell.rightPanel.tabs.files": "Fichiers",
|
"instanceShell.rightPanel.tabs.files": "Fichiers",
|
||||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||||
@@ -112,6 +113,10 @@ export const instanceMessages = {
|
|||||||
"instanceShell.sessionChanges.filesChanged": "{count} fichiers modifiés",
|
"instanceShell.sessionChanges.filesChanged": "{count} fichiers modifiés",
|
||||||
"instanceShell.sessionChanges.actions.show": "Afficher les changements",
|
"instanceShell.sessionChanges.actions.show": "Afficher les changements",
|
||||||
|
|
||||||
|
"instanceShell.gitChanges.loading": "Chargement des changements Git...",
|
||||||
|
"instanceShell.gitChanges.empty": "Aucun changement Git pour l'instant.",
|
||||||
|
"instanceShell.gitChanges.deleted": "Supprimé",
|
||||||
|
|
||||||
"instanceShell.filesShell.fileListTitle": "Liste des fichiers",
|
"instanceShell.filesShell.fileListTitle": "Liste des fichiers",
|
||||||
"instanceShell.filesShell.mobileSelectorLabel": "Sélectionner un fichier",
|
"instanceShell.filesShell.mobileSelectorLabel": "Sélectionner un fichier",
|
||||||
"instanceShell.filesShell.mobileSelectorEmpty": "Sélectionnez un fichier",
|
"instanceShell.filesShell.mobileSelectorEmpty": "Sélectionnez un fichier",
|
||||||
|
|||||||
6
packages/ui/src/lib/i18n/messages/he/advancedSettings.ts
Normal file
6
packages/ui/src/lib/i18n/messages/he/advancedSettings.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const advancedSettingsMessages = {
|
||||||
|
"advancedSettings.title": "הגדרות מתקדמות",
|
||||||
|
"advancedSettings.environmentVariables.title": "משתני סביבה",
|
||||||
|
"advancedSettings.environmentVariables.subtitle": "מוחלים בכל פעם שמופע OpenCode חדש מופעל",
|
||||||
|
"advancedSettings.actions.close": "סגור",
|
||||||
|
} as const
|
||||||
42
packages/ui/src/lib/i18n/messages/he/app.ts
Normal file
42
packages/ui/src/lib/i18n/messages/he/app.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const appMessages = {
|
||||||
|
"app.launchError.title": "לא ניתן להפעיל את OpenCode",
|
||||||
|
"app.launchError.description": "לא הצלחנו להפעיל את קובץ ה-OpenCode שנבחר. בדוק את פלט השגיאה למטה או בחר קובץ בינארי אחר מהגדרות OpenCode.",
|
||||||
|
"app.launchError.binaryPathLabel": "נתיב הקובץ הבינארי",
|
||||||
|
"app.launchError.errorOutputLabel": "פלט שגיאה",
|
||||||
|
"app.launchError.openAdvancedSettings": "פתח הגדרות OpenCode",
|
||||||
|
"app.launchError.close": "סגור",
|
||||||
|
"app.launchError.closeTitle": "סגור (Esc)",
|
||||||
|
"app.launchError.fallbackMessage": "הפעלת סביבת העבודה נכשלה",
|
||||||
|
|
||||||
|
"app.stopInstance.confirmMessage": "לעצור את מופע OpenCode? פעולה זו תעצור את השרת.",
|
||||||
|
"app.stopInstance.title": "עצור מופע",
|
||||||
|
"app.stopInstance.confirmLabel": "עצור",
|
||||||
|
"app.stopInstance.cancelLabel": "המשך להריץ",
|
||||||
|
|
||||||
|
"emptyState.logoAlt": "לוגו CodeNomad",
|
||||||
|
"emptyState.brandTitle": "CodeNomad",
|
||||||
|
"emptyState.tagline": "בחר תיקייה כדי להתחיל לתכנת עם AI",
|
||||||
|
"emptyState.actions.selectFolder": "בחר תיקייה",
|
||||||
|
"emptyState.actions.selecting": "בוחר...",
|
||||||
|
"emptyState.keyboardShortcut": "קיצור מקלדת: {shortcut}",
|
||||||
|
"emptyState.examples": "דוגמאות: {example}",
|
||||||
|
"emptyState.multipleInstances": "ניתן לפתוח מספר מופעים של אותה תיקייה",
|
||||||
|
|
||||||
|
"releases.upgradeRequired.title": "נדרש שדרוג",
|
||||||
|
"releases.upgradeRequired.message.withVersion": "שדרג ל-CodeNomad {version} כדי להשתמש בממשק המעודכן.",
|
||||||
|
"releases.upgradeRequired.message.noVersion": "שדרג את CodeNomad כדי להשתמש בממשק המעודכן.",
|
||||||
|
"releases.upgradeRequired.action.getUpdate": "קבל עדכון",
|
||||||
|
|
||||||
|
"releases.uiUpdated.title": "הממשק עודכן",
|
||||||
|
"releases.uiUpdated.message": "הממשק עודכן לגרסה {version}.",
|
||||||
|
|
||||||
|
"releases.devUpdateAvailable.title": "גרסת פיתוח זמינה",
|
||||||
|
"releases.devUpdateAvailable.message": "גרסת פיתוח חדשה זמינה: {version}.",
|
||||||
|
"releases.devUpdateAvailable.action": "צפה בגרסה",
|
||||||
|
|
||||||
|
"theme.mode.system": "מערכת",
|
||||||
|
"theme.mode.light": "בהיר",
|
||||||
|
"theme.mode.dark": "כהה",
|
||||||
|
"theme.toggle.title": "ערכת נושא: {mode}",
|
||||||
|
"theme.toggle.ariaLabel": "ערכת נושא: {mode}",
|
||||||
|
} as const
|
||||||
176
packages/ui/src/lib/i18n/messages/he/commands.ts
Normal file
176
packages/ui/src/lib/i18n/messages/he/commands.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
export const commandMessages = {
|
||||||
|
"commandPalette.title": "לוח פקודות",
|
||||||
|
"commandPalette.description": "חיפוש והפעלה של פקודות",
|
||||||
|
"commandPalette.searchPlaceholder": "הקלד פקודה או חיפוש...",
|
||||||
|
"commandPalette.empty": "לא נמצאו פקודות עבור \"{query}\"",
|
||||||
|
"commandPalette.category.customCommands": "פקודות מותאמות אישית",
|
||||||
|
"commandPalette.category.instance": "מופע",
|
||||||
|
"commandPalette.category.session": "סשן",
|
||||||
|
"commandPalette.category.agentModel": "סוכן ומודל",
|
||||||
|
"commandPalette.category.inputFocus": "קלט ופוקוס",
|
||||||
|
"commandPalette.category.system": "מערכת",
|
||||||
|
"commandPalette.category.other": "אחר",
|
||||||
|
|
||||||
|
"commands.newInstance.label": "מופע חדש",
|
||||||
|
"commands.newInstance.description": "פתח בורר תיקיות ליצירת מופע חדש",
|
||||||
|
"commands.newInstance.keywords": "תיקייה, פרויקט, סביבת עבודה",
|
||||||
|
|
||||||
|
"commands.closeInstance.label": "סגור מופע",
|
||||||
|
"commands.closeInstance.description": "עצור את השרת של המופע הנוכחי",
|
||||||
|
"commands.closeInstance.keywords": "עצור, סגור",
|
||||||
|
|
||||||
|
"commands.nextInstance.label": "מופע הבא",
|
||||||
|
"commands.nextInstance.description": "עבור למופע הבא",
|
||||||
|
"commands.nextInstance.keywords": "החלף, נווט",
|
||||||
|
|
||||||
|
"commands.previousInstance.label": "מופע קודם",
|
||||||
|
"commands.previousInstance.description": "עבור למופע הקודם",
|
||||||
|
"commands.previousInstance.keywords": "החלף, נווט",
|
||||||
|
|
||||||
|
"commands.newSession.label": "סשן חדש",
|
||||||
|
"commands.newSession.description": "צור סשן הורה חדש",
|
||||||
|
"commands.newSession.keywords": "צור, התחל",
|
||||||
|
|
||||||
|
"commands.closeSession.label": "סגור סשן",
|
||||||
|
"commands.closeSession.description": "סגור את סשן ההורה הנוכחי",
|
||||||
|
"commands.closeSession.keywords": "סגור, עצור",
|
||||||
|
|
||||||
|
"commands.scrubSessions.label": "נקה סשנים",
|
||||||
|
"commands.scrubSessions.description": "הסר סשנים ריקים, סשני תת-סוכן שסיימו את משימתם הראשית, וסשני פיצול מיותרים.",
|
||||||
|
"commands.scrubSessions.keywords": "ניקוי, ריק, סשנים, הסר, מחק",
|
||||||
|
|
||||||
|
"commands.instanceInfo.label": "מידע על מופע",
|
||||||
|
"commands.instanceInfo.description": "פתח את סקירת המופע ללוגים וסטטוס",
|
||||||
|
"commands.instanceInfo.keywords": "מידע, לוגים, קונסולה, פלט",
|
||||||
|
|
||||||
|
"commands.nextSession.label": "סשן הבא",
|
||||||
|
"commands.nextSession.description": "עבור לסשן הבא",
|
||||||
|
"commands.nextSession.keywords": "החלף, נווט",
|
||||||
|
|
||||||
|
"commands.previousSession.label": "סשן קודם",
|
||||||
|
"commands.previousSession.description": "עבור לסשן הקודם",
|
||||||
|
"commands.previousSession.keywords": "החלף, נווט",
|
||||||
|
|
||||||
|
"commands.compactSession.label": "סכם סשן",
|
||||||
|
"commands.compactSession.description": "סכם ודחוס את הסשן הנוכחי",
|
||||||
|
"commands.compactSession.keywords": "סיכום, דחיסה",
|
||||||
|
"commands.compactSession.errorFallback": "סיכום הסשן נכשל",
|
||||||
|
"commands.compactSession.alert.title": "הסיכום נכשל",
|
||||||
|
"commands.compactSession.alert.message": "הסיכום נכשל: {message}",
|
||||||
|
|
||||||
|
"commands.undoLastMessage.label": "בטל הודעה אחרונה",
|
||||||
|
"commands.undoLastMessage.description": "בטל את ההודעה האחרונה",
|
||||||
|
"commands.undoLastMessage.keywords": "חזרה, ביטול",
|
||||||
|
"commands.undoLastMessage.none.title": "אין פעולות לביטול",
|
||||||
|
"commands.undoLastMessage.none.message": "אין מה לבטל",
|
||||||
|
"commands.undoLastMessage.failed.title": "הביטול נכשל",
|
||||||
|
"commands.undoLastMessage.failed.message": "ביטול ההודעה נכשל",
|
||||||
|
|
||||||
|
"commands.openModelSelector.label": "פתח בורר מודלים",
|
||||||
|
"commands.openModelSelector.description": "בחר מודל אחר",
|
||||||
|
"commands.openModelSelector.keywords": "מודל, llm, ai",
|
||||||
|
|
||||||
|
"commands.selectModelVariant.label": "בחר גרסת מודל",
|
||||||
|
"commands.selectModelVariant.description": "בחר רמת מאמץ חשיבה למודל הנוכחי",
|
||||||
|
"commands.selectModelVariant.keywords": "גרסה, חשיבה, מאמץ",
|
||||||
|
|
||||||
|
"commands.openAgentSelector.label": "פתח בורר סוכנים",
|
||||||
|
"commands.openAgentSelector.description": "בחר סוכן אחר",
|
||||||
|
"commands.openAgentSelector.keywords": "סוכן, מצב",
|
||||||
|
|
||||||
|
"commands.clearInput.label": "נקה קלט",
|
||||||
|
"commands.clearInput.description": "נקה את תיבת הטקסט של הפקודה",
|
||||||
|
"commands.clearInput.keywords": "נקה, אפס",
|
||||||
|
|
||||||
|
"commands.promptSubmitShortcut.label.default": "Enter: שורה חדשה, Cmd/Ctrl+Enter: שלח פקודה",
|
||||||
|
"commands.promptSubmitShortcut.label.swapped": "Enter: שלח פקודה, Cmd/Ctrl+Enter: שורה חדשה",
|
||||||
|
"commands.promptSubmitShortcut.description": "החלף את התנהגות Enter ו-Cmd/Ctrl+Enter בקלט הפקודה",
|
||||||
|
"commands.promptSubmitShortcut.keywords": "enter, cmd, ctrl, שלח, שורה חדשה, קיצור",
|
||||||
|
|
||||||
|
"commands.thinkingBlocks.label.show": "הצג חשיבה",
|
||||||
|
"commands.thinkingBlocks.label.hide": "הסתר חשיבה",
|
||||||
|
"commands.thinkingBlocks.description": "הצג או הסתר קטעי חשיבה של ה-AI",
|
||||||
|
"commands.thinkingBlocks.keywords": "חשיבה, הצג, הסתר",
|
||||||
|
|
||||||
|
"commands.timelineToolCalls.label.show": "הצג קריאות כלי בציר הזמן",
|
||||||
|
"commands.timelineToolCalls.label.hide": "הסתר קריאות כלי בציר הזמן",
|
||||||
|
"commands.timelineToolCalls.description": "הצג/הסתר קריאות כלי בציר הודעות",
|
||||||
|
"commands.timelineToolCalls.keywords": "ציר זמן, כלי, הצג, הסתר",
|
||||||
|
|
||||||
|
"commands.keyboardShortcutHints.label.show": "הצג רמזי קיצורי מקלדת",
|
||||||
|
"commands.keyboardShortcutHints.label.hide": "הסתר רמזי קיצורי מקלדת",
|
||||||
|
"commands.keyboardShortcutHints.description": "הצג או הסתר רמזי קיצורי מקלדת בכל הממשק",
|
||||||
|
"commands.keyboardShortcutHints.description.disabledWeb": "מושבת בממשק Web (רמזי קיצורים תמיד מוסתרים)",
|
||||||
|
"commands.keyboardShortcutHints.keywords": "קיצור, מקלדת, רמזים",
|
||||||
|
|
||||||
|
"commands.common.expanded": "פרוס",
|
||||||
|
"commands.common.collapsed": "מכווץ",
|
||||||
|
"commands.common.visible": "גלוי",
|
||||||
|
"commands.common.hidden": "מוסתר",
|
||||||
|
"commands.common.enabled": "מופעל",
|
||||||
|
"commands.common.disabled": "מושבת",
|
||||||
|
|
||||||
|
"commands.thinkingBlocksDefault.label": "תצוגת חשיבה: {state}",
|
||||||
|
"commands.thinkingBlocksDefault.description": "כווץ / פרוס קטעי חשיבה של ה-AI",
|
||||||
|
"commands.thinkingBlocksDefault.keywords": "חשיבה, פרוס, כווץ, ברירת מחדל",
|
||||||
|
|
||||||
|
"commands.diffViewSplit.label": "השתמש בתצוגת diff מפוצלת",
|
||||||
|
"commands.diffViewSplit.description": "הצג diff של קריאות כלי זה לצד זה",
|
||||||
|
"commands.diffViewSplit.keywords": "diff, מפוצל, תצוגה",
|
||||||
|
|
||||||
|
"commands.diffViewUnified.label": "השתמש בתצוגת diff מאוחדת",
|
||||||
|
"commands.diffViewUnified.description": "הצג diff של קריאות כלי בשורה אחת",
|
||||||
|
"commands.diffViewUnified.keywords": "diff, מאוחד, תצוגה",
|
||||||
|
|
||||||
|
"commands.toolOutputsDefault.label": "ברירת מחדל לפלטי כלים · {state}",
|
||||||
|
"commands.toolOutputsDefault.description": "החלף ברירת מחדל לפריסת פלטי כלים",
|
||||||
|
"commands.toolOutputsDefault.keywords": "כלי, פלט, פרוס, כווץ",
|
||||||
|
|
||||||
|
"commands.diagnosticsDefault.label": "ברירת מחדל לאבחון · {state}",
|
||||||
|
"commands.diagnosticsDefault.description": "החלף ברירת מחדל לפריסת פלט אבחון",
|
||||||
|
"commands.diagnosticsDefault.keywords": "אבחון, פרוס, כווץ",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "נראות קלטי כלים · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "הגדר נראות ברירת מחדל לארגומנטים של קריאות כלי",
|
||||||
|
"commands.toolInputsVisibility.keywords": "כלי, קלטים, ארגומנטים, נראות, הסתר, הצג",
|
||||||
|
|
||||||
|
"commands.tokenUsageDisplay.label": "תצוגת שימוש בטוקנים · {state}",
|
||||||
|
"commands.tokenUsageDisplay.description": "הצג או הסתר נתוני טוקנים ועלות להודעות הסוכן",
|
||||||
|
"commands.tokenUsageDisplay.keywords": "טוקן, שימוש, עלות, נתונים",
|
||||||
|
|
||||||
|
"commands.autoCleanupBlankSessions.label": "ניקוי אוטומטי של סשנים ריקים · {state}",
|
||||||
|
"commands.autoCleanupBlankSessions.description": "נקה אוטומטית סשנים ריקים בעת יצירת סשנים חדשים",
|
||||||
|
"commands.autoCleanupBlankSessions.keywords": "אוטומטי, ניקוי, ריק, סשנים",
|
||||||
|
|
||||||
|
"commands.showHelp.label": "הצג עזרה",
|
||||||
|
"commands.showHelp.description": "הצג קיצורי מקלדת ועזרה",
|
||||||
|
"commands.showHelp.keywords": "קיצורים, עזרה",
|
||||||
|
|
||||||
|
"commands.custom.argumentsPrompt.message": "ארגומנטים עבור /{name}",
|
||||||
|
"commands.custom.argumentsPrompt.title": "פקודה מותאמת אישית",
|
||||||
|
"commands.custom.argumentsPrompt.inputLabel": "ארגומנטים",
|
||||||
|
"commands.custom.argumentsPrompt.inputPlaceholder": "למשל: foo bar",
|
||||||
|
"commands.custom.argumentsPrompt.confirmLabel": "הפעל",
|
||||||
|
"commands.custom.argumentsPrompt.cancelLabel": "ביטול",
|
||||||
|
"commands.custom.argumentsPrompt.openFailed.message": "פתיחת תיבת ארגומנטים נכשלה.",
|
||||||
|
"commands.custom.argumentsPrompt.openFailed.title": "ארגומנטים לפקודה",
|
||||||
|
"commands.custom.entries.descriptionFallback": "פקודה מותאמת אישית",
|
||||||
|
"commands.custom.sessionRequired.message": "בחר סשן לפני הפעלת פקודה מותאמת אישית.",
|
||||||
|
"commands.custom.sessionRequired.title": "נדרש סשן",
|
||||||
|
"commands.custom.runFailed.message": "הפעלת הפקודה המותאמת אישית נכשלה. בדוק את הקונסולה לפרטים.",
|
||||||
|
"commands.custom.runFailed.title": "הפקודה נכשלה",
|
||||||
|
|
||||||
|
"unifiedPicker.loading.searching": "מחפש...",
|
||||||
|
"unifiedPicker.loading.loadingWorkspace": "טוען סביבת עבודה...",
|
||||||
|
"unifiedPicker.title.command": "בחר פקודה",
|
||||||
|
"unifiedPicker.title.mention": "בחר סוכן או קובץ",
|
||||||
|
"unifiedPicker.empty": "לא נמצאו תוצאות",
|
||||||
|
"unifiedPicker.sections.commands": "פקודות",
|
||||||
|
"unifiedPicker.sections.agents": "סוכנים",
|
||||||
|
"unifiedPicker.sections.files": "קבצים",
|
||||||
|
"unifiedPicker.sections.workspaceRoot": "שורש סביבת העבודה",
|
||||||
|
"unifiedPicker.badge.subagent": "תת-סוכן",
|
||||||
|
"unifiedPicker.footer.navigate": "ניווט",
|
||||||
|
"unifiedPicker.footer.select": "בחירה",
|
||||||
|
"unifiedPicker.footer.close": "סגירה",
|
||||||
|
} as const
|
||||||
16
packages/ui/src/lib/i18n/messages/he/dialogs.ts
Normal file
16
packages/ui/src/lib/i18n/messages/he/dialogs.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const dialogMessages = {
|
||||||
|
"alertDialog.fallbackTitle.info": "לתשומת לבך",
|
||||||
|
"alertDialog.fallbackTitle.warning": "נא לבדוק",
|
||||||
|
"alertDialog.fallbackTitle.error": "משהו השתבש",
|
||||||
|
"alertDialog.actions.confirm": "אישור",
|
||||||
|
"alertDialog.actions.run": "הפעל",
|
||||||
|
"alertDialog.actions.ok": "אישור",
|
||||||
|
"alertDialog.actions.cancel": "ביטול",
|
||||||
|
"alertDialog.prompt.inputLabel": "קלט",
|
||||||
|
|
||||||
|
"backgroundProcessOutputDialog.title": "פלט תהליך רקע",
|
||||||
|
"backgroundProcessOutputDialog.actions.close": "סגור",
|
||||||
|
"backgroundProcessOutputDialog.loading": "טוען פלט...",
|
||||||
|
"backgroundProcessOutputDialog.truncatedNotice": "הפלט קוצר לצורך התצוגה.",
|
||||||
|
"backgroundProcessOutputDialog.loadErrorFallback": "טעינת הפלט נכשלה.",
|
||||||
|
} as const
|
||||||
43
packages/ui/src/lib/i18n/messages/he/filesystem.ts
Normal file
43
packages/ui/src/lib/i18n/messages/he/filesystem.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export const filesystemMessages = {
|
||||||
|
"directoryBrowser.defaultDescription": "עיון בתיקיות תחת שורש סביבת העבודה המוגדר.",
|
||||||
|
"directoryBrowser.close": "סגור",
|
||||||
|
"directoryBrowser.currentFolder": "תיקייה נוכחית",
|
||||||
|
"directoryBrowser.selectCurrent": "בחר נוכחית",
|
||||||
|
"directoryBrowser.newFolder": "תיקייה חדשה",
|
||||||
|
"directoryBrowser.creating": "יוצר…",
|
||||||
|
"directoryBrowser.loadingFolders": "טוען תיקיות…",
|
||||||
|
"directoryBrowser.noFolders": "אין תיקיות זמינות.",
|
||||||
|
"directoryBrowser.upOneLevel": "עלה רמה אחת",
|
||||||
|
"directoryBrowser.select": "בחר",
|
||||||
|
"directoryBrowser.load.errorFallback": "לא ניתן לטעון את מערכת הקבצים",
|
||||||
|
"directoryBrowser.createFolder.promptMessage": "צור תיקייה חדשה בספרייה הנוכחית.",
|
||||||
|
"directoryBrowser.createFolder.title": "תיקייה חדשה",
|
||||||
|
"directoryBrowser.createFolder.inputLabel": "שם תיקייה",
|
||||||
|
"directoryBrowser.createFolder.inputPlaceholder": "למשל: my-new-project",
|
||||||
|
"directoryBrowser.createFolder.confirmLabel": "צור",
|
||||||
|
"directoryBrowser.createFolder.cancelLabel": "ביטול",
|
||||||
|
"directoryBrowser.createFolder.invalidNameMessage": "נא להזין שם תיקייה יחיד.",
|
||||||
|
"directoryBrowser.createFolder.invalidNameDetail": "שמות תיקיות אינם יכולים לכלול נטויות, '..', או '~'.",
|
||||||
|
"directoryBrowser.createFolder.errorFallback": "יצירת התיקייה נכשלה",
|
||||||
|
|
||||||
|
"filesystemBrowser.descriptionFallback": "חפש נתיב תחת שורש סביבת העבודה המוגדר.",
|
||||||
|
"filesystemBrowser.rootLabel": "שורש: {root}",
|
||||||
|
"filesystemBrowser.actions.close": "סגור",
|
||||||
|
"filesystemBrowser.actions.retry": "נסה שוב",
|
||||||
|
"filesystemBrowser.actions.select": "בחר",
|
||||||
|
"filesystemBrowser.filterLabel": "סינון",
|
||||||
|
"filesystemBrowser.search.placeholder.directories": "חפש תיקיות",
|
||||||
|
"filesystemBrowser.search.placeholder.files": "חפש קבצים",
|
||||||
|
"filesystemBrowser.currentFolder.label": "תיקייה נוכחית",
|
||||||
|
"filesystemBrowser.currentFolder.selectCurrent": "בחר נוכחית",
|
||||||
|
"filesystemBrowser.loading.filesystem": "מערכת קבצים",
|
||||||
|
"filesystemBrowser.loading.workspaceRoot": "שורש סביבת עבודה",
|
||||||
|
"filesystemBrowser.loading.loadingWithPath": "טוען {path}…",
|
||||||
|
"filesystemBrowser.empty.noEntries": "לא נמצאו רשומות.",
|
||||||
|
"filesystemBrowser.navigation.upOneLevel": "עלה רמה אחת",
|
||||||
|
"filesystemBrowser.hints.navigate": "ניווט",
|
||||||
|
"filesystemBrowser.hints.select": "בחירה",
|
||||||
|
"filesystemBrowser.hints.close": "סגירה",
|
||||||
|
"filesystemBrowser.errors.loadFilesystemFallback": "לא ניתן לטעון את מערכת הקבצים",
|
||||||
|
"filesystemBrowser.errors.openDirectoryFallback": "לא ניתן לפתוח את הספרייה",
|
||||||
|
} as const
|
||||||
42
packages/ui/src/lib/i18n/messages/he/folderSelection.ts
Normal file
42
packages/ui/src/lib/i18n/messages/he/folderSelection.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const folderSelectionMessages = {
|
||||||
|
"folderSelection.language.ariaLabel": "שפה",
|
||||||
|
|
||||||
|
"folderSelection.logoAlt": "לוגו CodeNomad",
|
||||||
|
"folderSelection.tagline": "בחר תיקייה כדי להתחיל לתכנת עם AI",
|
||||||
|
|
||||||
|
"folderSelection.links.github": "CodeNomad GitHub",
|
||||||
|
"folderSelection.links.githubStars": "כוכבי CodeNomad ב-GitHub",
|
||||||
|
"folderSelection.links.discord": "CodeNomad Discord",
|
||||||
|
|
||||||
|
"folderSelection.empty.title": "אין תיקיות אחרונות",
|
||||||
|
"folderSelection.empty.description": "עיין בתיקייה כדי להתחיל",
|
||||||
|
|
||||||
|
"folderSelection.recent.title": "תיקיות אחרונות",
|
||||||
|
"folderSelection.recent.subtitle.one": "תיקייה אחת זמינה",
|
||||||
|
"folderSelection.recent.subtitle.other": "{count} תיקיות זמינות",
|
||||||
|
"folderSelection.recent.remove": "הסר מהרשימה האחרונה",
|
||||||
|
|
||||||
|
"folderSelection.browse.title": "עיון בתיקייה",
|
||||||
|
"folderSelection.browse.subtitle": "בחר כל תיקייה במחשב שלך",
|
||||||
|
"folderSelection.browse.button": "עיון בתיקיות",
|
||||||
|
"folderSelection.browse.buttonOpening": "פותח...",
|
||||||
|
|
||||||
|
"folderSelection.advancedSettings": "הגדרות מתקדמות",
|
||||||
|
"folderSelection.opencode": "OpenCode",
|
||||||
|
|
||||||
|
"folderSelection.hints.navigate": "ניווט",
|
||||||
|
"folderSelection.hints.select": "בחירה",
|
||||||
|
"folderSelection.hints.remove": "הסרה",
|
||||||
|
"folderSelection.hints.browse": "עיון",
|
||||||
|
|
||||||
|
"folderSelection.loading.title": "מפעיל מופע...",
|
||||||
|
"folderSelection.loading.subtitle": "המתן בזמן שאנו מכינים את סביבת העבודה שלך.",
|
||||||
|
|
||||||
|
"folderSelection.drop.title": "שחרר תיקייה כדי לפתוח אותה",
|
||||||
|
"folderSelection.drop.subtitle": "התחל מופע חדש בתיקייה שנשחררה.",
|
||||||
|
"folderSelection.drop.invalidTitle": "לא ניתן לפתוח את הפריט שנשחרר",
|
||||||
|
"folderSelection.drop.invalidMessage": "שחרר תיקייה כדי להתחיל מופע חדש.",
|
||||||
|
|
||||||
|
"folderSelection.dialog.title": "בחר סביבת עבודה",
|
||||||
|
"folderSelection.dialog.description": "בחר סביבת עבודה כדי להתחיל לתכנת.",
|
||||||
|
} as const
|
||||||
36
packages/ui/src/lib/i18n/messages/he/index.ts
Normal file
36
packages/ui/src/lib/i18n/messages/he/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { advancedSettingsMessages } from "./advancedSettings"
|
||||||
|
import { appMessages } from "./app"
|
||||||
|
import { commandMessages } from "./commands"
|
||||||
|
import { dialogMessages } from "./dialogs"
|
||||||
|
import { filesystemMessages } from "./filesystem"
|
||||||
|
import { folderSelectionMessages } from "./folderSelection"
|
||||||
|
import { instanceMessages } from "./instance"
|
||||||
|
import { loadingScreenMessages } from "./loadingScreen"
|
||||||
|
import { logMessages } from "./logs"
|
||||||
|
import { markdownMessages } from "./markdown"
|
||||||
|
import { messagingMessages } from "./messaging"
|
||||||
|
import { remoteAccessMessages } from "./remoteAccess"
|
||||||
|
import { sessionMessages } from "./session"
|
||||||
|
import { settingsMessages } from "./settings"
|
||||||
|
import { timeMessages } from "./time"
|
||||||
|
import { toolCallMessages } from "./toolCall"
|
||||||
|
import { mergeMessageParts } from "../merge"
|
||||||
|
|
||||||
|
export const heMessages = mergeMessageParts(
|
||||||
|
folderSelectionMessages,
|
||||||
|
advancedSettingsMessages,
|
||||||
|
loadingScreenMessages,
|
||||||
|
timeMessages,
|
||||||
|
appMessages,
|
||||||
|
dialogMessages,
|
||||||
|
filesystemMessages,
|
||||||
|
instanceMessages,
|
||||||
|
logMessages,
|
||||||
|
sessionMessages,
|
||||||
|
messagingMessages,
|
||||||
|
toolCallMessages,
|
||||||
|
markdownMessages,
|
||||||
|
settingsMessages,
|
||||||
|
remoteAccessMessages,
|
||||||
|
commandMessages,
|
||||||
|
)
|
||||||
166
packages/ui/src/lib/i18n/messages/he/instance.ts
Normal file
166
packages/ui/src/lib/i18n/messages/he/instance.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
export const instanceMessages = {
|
||||||
|
"instanceTabs.new.title": "מופע חדש (Cmd/Ctrl+N)",
|
||||||
|
"instanceTabs.new.ariaLabel": "מופע חדש",
|
||||||
|
"instanceTabs.remote.title": "חיבור מרוחק",
|
||||||
|
"instanceTabs.remote.ariaLabel": "חיבור מרוחק",
|
||||||
|
|
||||||
|
"instanceInfo.title": "מידע על המופע",
|
||||||
|
"instanceInfo.labels.folder": "תיקייה",
|
||||||
|
"instanceInfo.labels.project": "פרויקט",
|
||||||
|
"instanceInfo.labels.versionControl": "בקרת גרסאות",
|
||||||
|
"instanceInfo.labels.opencodeVersion": "גרסת OpenCode",
|
||||||
|
"instanceInfo.labels.binaryPath": "נתיב קובץ בינארי",
|
||||||
|
"instanceInfo.labels.environmentVariables": "משתני סביבה ({count})",
|
||||||
|
"instanceInfo.loading": "טוען...",
|
||||||
|
"instanceInfo.server.title": "שרת",
|
||||||
|
"instanceInfo.server.port": "פורט:",
|
||||||
|
"instanceInfo.server.pid": "PID:",
|
||||||
|
"instanceInfo.server.status": "סטטוס:",
|
||||||
|
|
||||||
|
"instanceTab.status.permission": "ממתין לאישור",
|
||||||
|
"instanceTab.status.compacting": "מסכם",
|
||||||
|
"instanceTab.status.working": "עובד",
|
||||||
|
"instanceTab.status.idle": "מוכן",
|
||||||
|
"instanceTab.status.ariaLabel": "סטטוס מופע: {status}",
|
||||||
|
"instanceTab.actions.close.ariaLabel": "סגור מופע",
|
||||||
|
|
||||||
|
"instanceShell.leftPanel.sessionsTitle": "סשנים",
|
||||||
|
"instanceShell.leftPanel.instanceInfo": "מידע על המופע",
|
||||||
|
|
||||||
|
"instanceShell.leftDrawer.pin": "נעץ מגירה שמאלית",
|
||||||
|
"instanceShell.leftDrawer.unpin": "שחרר נעיצת מגירה שמאלית",
|
||||||
|
"instanceShell.leftDrawer.toggle.pinned": "המגירה השמאלית נעוצה",
|
||||||
|
"instanceShell.leftDrawer.toggle.open": "פתח מגירה שמאלית",
|
||||||
|
"instanceShell.leftDrawer.toggle.close": "סגור מגירה שמאלית",
|
||||||
|
|
||||||
|
"instanceShell.rightDrawer.pin": "נעץ מגירה ימנית",
|
||||||
|
"instanceShell.rightDrawer.unpin": "שחרר נעיצת מגירה ימנית",
|
||||||
|
"instanceShell.rightDrawer.toggle.pinned": "המגירה הימנית נעוצה",
|
||||||
|
"instanceShell.rightDrawer.toggle.open": "פתח מגירה ימנית",
|
||||||
|
"instanceShell.rightDrawer.toggle.close": "סגור מגירה ימנית",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "מסך מלא",
|
||||||
|
"instanceShell.fullscreen.exit": "יציאה ממסך מלא",
|
||||||
|
|
||||||
|
"instanceShell.metrics.usedLabel": "בשימוש",
|
||||||
|
"instanceShell.metrics.availableLabel": "זמין",
|
||||||
|
|
||||||
|
"instanceShell.commandPalette.openAriaLabel": "פתח לוח פקודות",
|
||||||
|
"instanceShell.commandPalette.button": "לוח פקודות",
|
||||||
|
|
||||||
|
"instanceShell.connection.ariaLabel": "חיבור {status}",
|
||||||
|
"instanceShell.connection.connected": "מחובר",
|
||||||
|
"instanceShell.connection.connecting": "מתחבר...",
|
||||||
|
"instanceShell.connection.disconnected": "מנותק",
|
||||||
|
"instanceShell.connection.unknown": "לא ידוע",
|
||||||
|
|
||||||
|
"instanceWelcome.shortcuts.newSession": "סשן חדש",
|
||||||
|
"instanceWelcome.empty.title": "אין סשנים קודמים",
|
||||||
|
"instanceWelcome.empty.description": "צור סשן חדש למטה כדי להתחיל",
|
||||||
|
"instanceWelcome.loading.title": "טוען סשנים",
|
||||||
|
"instanceWelcome.loading.description": "מאחזר את הסשנים הקודמים שלך...",
|
||||||
|
"instanceWelcome.resume.title": "המשך סשן",
|
||||||
|
"instanceWelcome.resume.subtitle.one": "סשן אחד זמין",
|
||||||
|
"instanceWelcome.resume.subtitle.other": "{count} סשנים זמינים",
|
||||||
|
"instanceWelcome.session.untitled": "סשן ללא שם",
|
||||||
|
"instanceWelcome.new.title": "התחל סשן חדש",
|
||||||
|
"instanceWelcome.new.subtitle": "ישתמש אוטומטית בסוכן/מודל האחרון שלך",
|
||||||
|
"instanceWelcome.new.createButton": "צור סשן",
|
||||||
|
"instanceWelcome.overlay.close": "סגור",
|
||||||
|
"instanceWelcome.actions.viewInstanceInfo": "צפה במידע על המופע",
|
||||||
|
"instanceWelcome.actions.renameTitle": "שנה שם סשן",
|
||||||
|
"instanceWelcome.actions.deleteTitle": "מחק סשן",
|
||||||
|
"instanceWelcome.hints.navigate": "ניווט",
|
||||||
|
"instanceWelcome.hints.jump": "קפיצה",
|
||||||
|
"instanceWelcome.hints.firstLast": "ראשון/אחרון",
|
||||||
|
"instanceWelcome.hints.resume": "המשך",
|
||||||
|
"instanceWelcome.hints.delete": "מחיקה",
|
||||||
|
"instanceWelcome.toasts.renameError": "לא ניתן לשנות שם הסשן",
|
||||||
|
|
||||||
|
"instanceDisconnected.title": "המופע התנתק",
|
||||||
|
"instanceDisconnected.folderFallback": "סביבת עבודה זו",
|
||||||
|
"instanceDisconnected.reasonFallback": "השרת הפסיק להגיב",
|
||||||
|
"instanceDisconnected.description": "לא ניתן עוד להגיע ל-{folder}. סגור את הלשונית כדי להמשיך לעבוד.",
|
||||||
|
"instanceDisconnected.details.title": "פרטים",
|
||||||
|
"instanceDisconnected.details.folderLabel": "תיקייה:",
|
||||||
|
"instanceDisconnected.actions.closeInstance": "סגור מופע",
|
||||||
|
|
||||||
|
"instanceShell.empty.title": "לא נבחר סשן",
|
||||||
|
"instanceShell.empty.description": "בחר סשן לצפייה בהודעות",
|
||||||
|
|
||||||
|
"instanceShell.rightPanel.title": "לוח סטטוס",
|
||||||
|
"instanceShell.rightPanel.tabs.changes": "שינויי סשן",
|
||||||
|
"instanceShell.rightPanel.tabs.gitChanges": "שינויי Git",
|
||||||
|
"instanceShell.rightPanel.tabs.files": "קבצים",
|
||||||
|
"instanceShell.rightPanel.tabs.status": "סטטוס",
|
||||||
|
"instanceShell.rightPanel.tabs.ariaLabel": "לשוניות לוח ימני",
|
||||||
|
"instanceShell.rightPanel.actions.refresh": "רענן",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
||||||
|
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "מפת הדרכים של הסוכן לסשן זה. עוקב אחר משימות, תת-משימות וסטטוס השלמתן.",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses": "מעטפות רקע",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "תהליכים ממושכים שהופעלו על ידי הסוכן. ניתן לעקוב אחר פלטם, לעצור אותם או לסיים אותם.",
|
||||||
|
"instanceShell.rightPanel.sections.mcp": "שרתי MCP",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "שרתי Model Context Protocol המרחיבים את יכולות הסוכן עם כלים ושירותים חיצוניים.",
|
||||||
|
"instanceShell.rightPanel.sections.lsp": "שרתי LSP",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "שרתי Language Server Protocol המספקים בינת קוד, אבחון ותכונות ספציפיות לשפה.",
|
||||||
|
"instanceShell.rightPanel.sections.plugins": "תוספים",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "תוספים המתאימים אישית את הממשק ואת התנהגות השרת, ומוסיפים תכונות מעבר ל-MCP ו-LSP.",
|
||||||
|
|
||||||
|
"instanceShell.sessionChanges.noSessionSelected": "בחר סשן לצפייה בשינויים.",
|
||||||
|
"instanceShell.sessionChanges.loading": "מאחזר שינויי סשן...",
|
||||||
|
"instanceShell.sessionChanges.empty": "אין שינויי סשן עדיין.",
|
||||||
|
"instanceShell.sessionChanges.filesChanged": "{count} קבצים שונו",
|
||||||
|
"instanceShell.sessionChanges.actions.show": "הצג שינויים",
|
||||||
|
|
||||||
|
"instanceShell.filesShell.fileListTitle": "רשימת קבצים",
|
||||||
|
"instanceShell.filesShell.mobileSelectorLabel": "בחר קובץ",
|
||||||
|
"instanceShell.filesShell.mobileSelectorEmpty": "בחר קובץ",
|
||||||
|
"instanceShell.filesShell.viewerTitle": "מציג שינויים",
|
||||||
|
"instanceShell.filesShell.viewerPlaceholder": "תצוגת שינויים מפורטת תתווסף בשלב הבא.",
|
||||||
|
"instanceShell.filesShell.viewerEmpty": "לא נבחר קובץ.",
|
||||||
|
"instanceShell.filesShell.hideFiles": "הסתר קבצים",
|
||||||
|
"instanceShell.filesShell.showFiles": "הצג קבצים",
|
||||||
|
"instanceShell.gitChanges.noSessionSelected": "בחר סשן לצפייה בשינויי Git.",
|
||||||
|
"instanceShell.gitChanges.loading": "טוען שינויי Git…",
|
||||||
|
"instanceShell.gitChanges.empty": "אין שינויי Git עדיין.",
|
||||||
|
"instanceShell.diff.hideUnchanged": "הסתר אזורים ללא שינוי",
|
||||||
|
"instanceShell.diff.showFull": "הצג קובץ מלא",
|
||||||
|
"instanceShell.diff.switchToSplit": "עבור לתצוגה מפוצלת",
|
||||||
|
"instanceShell.diff.switchToUnified": "עבור לתצוגה מאוחדת",
|
||||||
|
"instanceShell.diff.enableWordWrap": "הפעל גלישת מילים",
|
||||||
|
"instanceShell.diff.disableWordWrap": "כבה גלישת מילים",
|
||||||
|
"instanceShell.worktree.create": "+ צור worktree",
|
||||||
|
|
||||||
|
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
||||||
|
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
||||||
|
|
||||||
|
"instanceShell.backgroundProcesses.empty": "אין תהליכי רקע.",
|
||||||
|
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
||||||
|
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
||||||
|
"instanceShell.backgroundProcesses.actions.output": "פלט",
|
||||||
|
"instanceShell.backgroundProcesses.actions.stop": "עצור",
|
||||||
|
"instanceShell.backgroundProcesses.actions.terminate": "סיים",
|
||||||
|
|
||||||
|
"versionPill.appWithVersion": "אפליקציה {version}",
|
||||||
|
"versionPill.ui": "ממשק",
|
||||||
|
"versionPill.uiWithVersion": "ממשק {version}",
|
||||||
|
"versionPill.source": " ({source})",
|
||||||
|
|
||||||
|
"opencodeBinarySelector.title": "קובץ בינארי של OpenCode",
|
||||||
|
"opencodeBinarySelector.subtitle": "בחר איזה קובץ הרצה OpenCode ישתמש",
|
||||||
|
"opencodeBinarySelector.customPath.placeholder": "הזן נתיב לקובץ בינארי של opencode…",
|
||||||
|
"opencodeBinarySelector.actions.add": "הוסף",
|
||||||
|
"opencodeBinarySelector.actions.browse": "עיין אחר קובץ בינארי…",
|
||||||
|
"opencodeBinarySelector.actions.removeTitle": "הסר קובץ בינארי",
|
||||||
|
"opencodeBinarySelector.badge.systemPath": "השתמש בקובץ בינארי מנתיב המערכת",
|
||||||
|
"opencodeBinarySelector.status.checkingVersions": "בודק גרסאות…",
|
||||||
|
"opencodeBinarySelector.status.checking": "בודק…",
|
||||||
|
"opencodeBinarySelector.dialog.title": "בחר קובץ בינארי של OpenCode",
|
||||||
|
"opencodeBinarySelector.dialog.description": "עיין בקבצים החשופים על ידי שרת ה-CLI.",
|
||||||
|
"opencodeBinarySelector.validation.invalidBinary": "קובץ בינארי לא תקין של OpenCode",
|
||||||
|
"opencodeBinarySelector.validation.alreadyValidating": "כבר מאמת",
|
||||||
|
"opencodeBinarySelector.display.systemPath": "{name} (נתיב מערכת)",
|
||||||
|
"opencodeBinarySelector.versionLabel": "v{version}",
|
||||||
|
} as const
|
||||||
17
packages/ui/src/lib/i18n/messages/he/loadingScreen.ts
Normal file
17
packages/ui/src/lib/i18n/messages/he/loadingScreen.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const loadingScreenMessages = {
|
||||||
|
"loadingScreen.logoAlt": "לוגו CodeNomad",
|
||||||
|
"loadingScreen.status.issue": "נתקלנו בבעיה",
|
||||||
|
"loadingScreen.actions.showAnother": "הצג עוד",
|
||||||
|
"loadingScreen.errors.missingRoot": "אלמנט השורש לטעינה לא נמצא",
|
||||||
|
|
||||||
|
"loadingScreen.phrases.neurons": "מחמם את הנוירונים של ה-AI…",
|
||||||
|
"loadingScreen.phrases.daydreaming": "משכנע את ה-AI להפסיק לחלום בהקיץ…",
|
||||||
|
"loadingScreen.phrases.goggles": "מצחצח את משקפי הקוד של ה-AI…",
|
||||||
|
"loadingScreen.phrases.reorganizingFiles": "מבקש מה-AI להפסיק לארגן מחדש את הקבצים שלך…",
|
||||||
|
"loadingScreen.phrases.coffee": "מאכיל את ה-AI עוד קפה…",
|
||||||
|
"loadingScreen.phrases.nodeModules": "מלמד את ה-AI לא למחוק node_modules (שוב)…",
|
||||||
|
"loadingScreen.phrases.actNatural": "אומר ל-AI להיראות טבעי לפני שתגיע…",
|
||||||
|
"loadingScreen.phrases.rewritingHistory": "מבקש מה-AI בבקשה להפסיק לשכתב היסטוריה…",
|
||||||
|
"loadingScreen.phrases.stretch": "מאפשר ל-AI להתמתח לפני ספרינט הקוד שלו…",
|
||||||
|
"loadingScreen.phrases.keyboardControl": "משכנע את ה-AI לתת לך שליטה על המקלדת…",
|
||||||
|
} as const
|
||||||
27
packages/ui/src/lib/i18n/messages/he/logs.ts
Normal file
27
packages/ui/src/lib/i18n/messages/he/logs.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const logMessages = {
|
||||||
|
"logsView.title": "לוגי שרת",
|
||||||
|
"logsView.actions.show": "הצג לוגי שרת",
|
||||||
|
"logsView.actions.hide": "הסתר לוגי שרת",
|
||||||
|
"logsView.envVars.title": "משתני סביבה ({count})",
|
||||||
|
"logsView.paused.title": "לוגי השרת מושהים",
|
||||||
|
"logsView.paused.description": "הפעל זרימה לצפייה בפעילות שרת OpenCode שלך.",
|
||||||
|
"logsView.empty.waiting": "ממתין לפלט שרת...",
|
||||||
|
"logsView.scrollToBottom": "גלול למטה",
|
||||||
|
|
||||||
|
"infoView.logs.title": "לוגי שרת",
|
||||||
|
"infoView.logs.actions.show": "הצג לוגי שרת",
|
||||||
|
"infoView.logs.actions.hide": "הסתר לוגי שרת",
|
||||||
|
"infoView.logs.paused.title": "לוגי השרת מושהים",
|
||||||
|
"infoView.logs.paused.description": "הפעל זרימה לצפייה בפעילות שרת OpenCode שלך.",
|
||||||
|
"infoView.logs.empty.waiting": "ממתין לפלט שרת...",
|
||||||
|
"infoView.logs.scrollToBottom": "גלול למטה",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "בטל מופע",
|
||||||
|
"infoView.dispose.actions.disposing": "מבטל...",
|
||||||
|
"infoView.dispose.confirm.title": "לבטל את המופע?",
|
||||||
|
"infoView.dispose.confirm.message": "פעולה זו מנקה את המצב השמור לפי פרויקט עבור ספרייה זו ומטעינה מחדש את המופע.",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "בטל",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "ביטול",
|
||||||
|
"infoView.dispose.toast.success": "המופע בוטל. מטעין מחדש...",
|
||||||
|
"infoView.dispose.toast.error": "ביטול המופע נכשל.",
|
||||||
|
} as const
|
||||||
7
packages/ui/src/lib/i18n/messages/he/markdown.ts
Normal file
7
packages/ui/src/lib/i18n/messages/he/markdown.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const markdownMessages = {
|
||||||
|
"markdown.codeBlock.copy.label": "העתק",
|
||||||
|
"markdown.codeBlock.copy.copied": "הועתק!",
|
||||||
|
"markdown.codeBlock.copy.failed": "נכשל",
|
||||||
|
|
||||||
|
"markdown.copy": "העתק",
|
||||||
|
} as const
|
||||||
141
packages/ui/src/lib/i18n/messages/he/messaging.ts
Normal file
141
packages/ui/src/lib/i18n/messages/he/messaging.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
export const messagingMessages = {
|
||||||
|
"messageListHeader.sidebar.openSessionListAriaLabel": "פתח רשימת סשנים",
|
||||||
|
"messageListHeader.metrics.usedLabel": "בשימוש",
|
||||||
|
"messageListHeader.metrics.availableLabel": "זמין",
|
||||||
|
"messageListHeader.commandPalette.ariaLabel": "פתח לוח פקודות",
|
||||||
|
"messageListHeader.commandPalette.button": "לוח פקודות",
|
||||||
|
"messageListHeader.connection.connected": "מחובר",
|
||||||
|
"messageListHeader.connection.connecting": "מתחבר...",
|
||||||
|
"messageListHeader.connection.disconnected": "מנותק",
|
||||||
|
|
||||||
|
"messageSection.empty.logoAlt": "לוגו CodeNomad",
|
||||||
|
"messageSection.empty.brandTitle": "CodeNomad",
|
||||||
|
"messageSection.empty.title": "התחל שיחה",
|
||||||
|
"messageSection.empty.description": "הקלד הודעה למטה או פתח את לוח הפקודות:",
|
||||||
|
"messageSection.empty.tips.commandPalette": "לוח פקודות",
|
||||||
|
"messageSection.empty.tips.askAboutCodebase": "שאל על בסיס הקוד שלך",
|
||||||
|
"messageSection.empty.tips.attachFilesPrefix": "צרף קבצים עם",
|
||||||
|
"messageSection.loading.messages": "טוען הודעות...",
|
||||||
|
"messageSection.scroll.toFirstAriaLabel": "גלול להודעה הראשונה",
|
||||||
|
"messageSection.scroll.toLatestAriaLabel": "גלול להודעה האחרונה",
|
||||||
|
"messageSection.quote.addAsQuote": "הוסף כציטוט",
|
||||||
|
"messageSection.quote.addAsCode": "הוסף כקוד",
|
||||||
|
"messageSection.quote.copy": "העתק",
|
||||||
|
"messageSection.quote.copied": "הועתק!",
|
||||||
|
"messageSection.quote.copyFailed": "ההעתקה נכשלה",
|
||||||
|
"messageTimeline.ariaLabel": "ציר זמן הודעות",
|
||||||
|
"messageTimeline.segment.user.label": "אתה",
|
||||||
|
"messageTimeline.segment.assistant.label": "סוכן",
|
||||||
|
"messageTimeline.segment.compaction.label": "סיכום",
|
||||||
|
"messageTimeline.tool.fallbackLabel": "קריאת כלי",
|
||||||
|
"messageTimeline.tooltip.userFallback": "הודעת משתמש",
|
||||||
|
"messageTimeline.tooltip.assistantFallback": "תגובת הסוכן",
|
||||||
|
"messageTimeline.tooltip.compaction.auto": "סיכום אוטומטי",
|
||||||
|
"messageTimeline.tooltip.compaction.manual": "סיכום ידני",
|
||||||
|
"messageTimeline.text.filePrefix": "[קובץ] {filename}",
|
||||||
|
"messageTimeline.text.attachment": "קובץ מצורף",
|
||||||
|
"messageBlock.tool.header": "קריאת כלי",
|
||||||
|
"messageBlock.tool.unknown": "לא ידוע",
|
||||||
|
"messageBlock.tool.goToSession.label": "עבור לסשן",
|
||||||
|
"messageBlock.tool.goToSession.title": "עבור לסשן",
|
||||||
|
"messageBlock.tool.goToSession.unavailableTitle": "הסשן עדיין אינו זמין",
|
||||||
|
"messageBlock.tool.deletePart.label": "מחק חלק",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "מוחק...",
|
||||||
|
"messageBlock.tool.deletePart.title": "מחק את פלט קריאת הכלי הזו",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "המחיקה נכשלה",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "מחיקת פלט קריאת הכלי נכשלה",
|
||||||
|
|
||||||
|
"messageBlock.compaction.ariaLabel": "סיכום סשן",
|
||||||
|
"messageBlock.compaction.autoLabel": "הסשן סוכם אוטומטית",
|
||||||
|
"messageBlock.compaction.manualLabel": "הסשן סוכם על ידך",
|
||||||
|
"messageBlock.usage.input": "קלט",
|
||||||
|
"messageBlock.usage.output": "פלט",
|
||||||
|
"messageBlock.usage.reasoning": "חשיבה",
|
||||||
|
"messageBlock.usage.cacheRead": "קריאת מטמון",
|
||||||
|
"messageBlock.usage.cacheWrite": "כתיבת מטמון",
|
||||||
|
"messageBlock.usage.cost": "עלות",
|
||||||
|
"messageBlock.step.agentLabel": "סוכן: {agent}",
|
||||||
|
"messageBlock.step.modelLabel": "מודל: {model}",
|
||||||
|
"messageBlock.reasoning.thinkingLabel": "חשיבה",
|
||||||
|
"messageBlock.reasoning.expandAriaLabel": "פרוס חשיבה",
|
||||||
|
"messageBlock.reasoning.collapseAriaLabel": "כווץ חשיבה",
|
||||||
|
"messageBlock.reasoning.indicator.hide": "הסתר",
|
||||||
|
"messageBlock.reasoning.indicator.view": "צפה",
|
||||||
|
"messageBlock.reasoning.detailsAriaLabel": "פרטי חשיבה",
|
||||||
|
|
||||||
|
"codeBlockInline.actions.copy": "העתק",
|
||||||
|
"codeBlockInline.actions.copied": "הועתק!",
|
||||||
|
|
||||||
|
"messageItem.speaker.you": "אתה",
|
||||||
|
"messageItem.speaker.assistant": "סוכן",
|
||||||
|
"messageItem.actions.revert": "בטל שינויים",
|
||||||
|
"messageItem.actions.revertTitle": "בטל שינויים עד כאן (מוחק הודעות)",
|
||||||
|
"messageItem.actions.fork": "פצל",
|
||||||
|
"messageItem.actions.forkTitle": "פצל מהודעה זו",
|
||||||
|
"messageItem.actions.copy": "העתק",
|
||||||
|
"messageItem.actions.copyTitle": "העתק הודעה",
|
||||||
|
"messageItem.actions.copied": "הועתק!",
|
||||||
|
"messageItem.actions.deleteMessage": "מחק הודעה (לא מבטל שינויים)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "מחק הודעות עד כאן (לא מבטל שינויים)",
|
||||||
|
"messageItem.actions.deletingMessage": "מוחק...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "המחיקה נכשלה",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "מחיקת ההודעה נכשלה",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "בחר הודעה למחיקה",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "פריטים נבחרים ({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "מחק פריטים נבחרים",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "בחר את כל ההודעות",
|
||||||
|
"messageSection.bulkDelete.moreOptionsTitle": "אפשרויות נוספות",
|
||||||
|
"messageSection.bulkDelete.selectionModeLabel": "בחירה",
|
||||||
|
"messageSection.bulkDelete.selectionModeAll": "הכל",
|
||||||
|
"messageSection.bulkDelete.selectionModeTools": "כלים בלבד",
|
||||||
|
"messageSection.bulkDelete.selectionHint.toggle": "בחר פריט",
|
||||||
|
"messageSection.bulkDelete.selectionHint.range": "בחר טווח",
|
||||||
|
"messageSection.bulkDelete.selectionHint.clear": "נקה בחירה",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "בטל בחירה",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "המחיקה נכשלה",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "מחיקת הפריטים הנבחרים נכשלה",
|
||||||
|
"messageItem.status.queued": "בתור",
|
||||||
|
"messageItem.status.generating": "מייצר...",
|
||||||
|
"messageItem.status.sending": "שולח...",
|
||||||
|
"messageItem.status.failedToSend": "שליחת ההודעה נכשלה",
|
||||||
|
"messagePart.actions.delete": "מחק חלק",
|
||||||
|
"messagePart.actions.deleting": "מוחק...",
|
||||||
|
"messagePart.actions.deleteTitle": "מחק פריט זה",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "המחיקה נכשלה",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "מחיקת הפריט נכשלה",
|
||||||
|
"messageItem.attachment.defaultName": "קובץ מצורף",
|
||||||
|
"messageItem.attachment.downloadAriaLabel": "הורד {name}",
|
||||||
|
"messageItem.agentMeta.agentLabel": "סוכן: {agent}",
|
||||||
|
"messageItem.agentMeta.modelLabel": "מודל: {model}",
|
||||||
|
"messageItem.errors.authenticationFallback": "שגיאת אימות",
|
||||||
|
"messageItem.errors.outputLengthExceeded": "אורך פלט ההודעה חרג מהמגבלה",
|
||||||
|
"messageItem.errors.requestAborted": "הבקשה בוטלה",
|
||||||
|
"messageItem.errors.unknownFallback": "אירעה שגיאה לא ידועה",
|
||||||
|
|
||||||
|
"attachmentChip.removeAriaLabel": "הסר קובץ מצורף",
|
||||||
|
|
||||||
|
"expandButton.toggleAriaLabel": "שנה גובה תיבת הקלט",
|
||||||
|
|
||||||
|
"promptInput.placeholder.shell": "הפעל פקודת מעטפת (Esc ליציאה)...",
|
||||||
|
"promptInput.placeholder.default": "הקלד הודעה, @file, @agent, או הדבק תמונות וטקסט...",
|
||||||
|
"promptInput.hints.shell.exit": "לצאת ממצב מעטפת",
|
||||||
|
"promptInput.hints.shell.enable": "מצב מעטפת",
|
||||||
|
"promptInput.hints.commands": "פקודות",
|
||||||
|
"promptInput.history.previousAriaLabel": "פקודה קודמת",
|
||||||
|
"promptInput.history.nextAriaLabel": "פקודה הבאה",
|
||||||
|
"promptInput.overlay.newLine": "שורה חדשה",
|
||||||
|
"promptInput.overlay.send": "שלח",
|
||||||
|
"promptInput.overlay.filesAgents": "קבצים/סוכנים",
|
||||||
|
"promptInput.overlay.history": "היסטוריה",
|
||||||
|
"promptInput.overlay.attachments": "• {count} קובץ/ים מצורף/ים",
|
||||||
|
"promptInput.overlay.shellModeActive": "מצב מעטפת פעיל",
|
||||||
|
"promptInput.overlay.press": "לחץ",
|
||||||
|
"promptInput.overlay.againToAbort": "שוב כדי לבטל את הסשן",
|
||||||
|
"promptInput.stopSession.ariaLabel": "עצור סשן",
|
||||||
|
"promptInput.stopSession.title": "עצור סשן",
|
||||||
|
"promptInput.send.ariaLabel": "שלח הודעה",
|
||||||
|
"promptInput.send.errorFallback": "שליחת ההודעה נכשלה",
|
||||||
|
"promptInput.send.errorTitle": "השליחה נכשלה",
|
||||||
|
} as const
|
||||||
51
packages/ui/src/lib/i18n/messages/he/remoteAccess.ts
Normal file
51
packages/ui/src/lib/i18n/messages/he/remoteAccess.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export const remoteAccessMessages = {
|
||||||
|
"remoteAccess.eyebrow": "גישה מרוחקת",
|
||||||
|
"remoteAccess.title": "התחבר ל-CodeNomad מרחוק",
|
||||||
|
"remoteAccess.subtitle": "השתמש בכתובות למטה כדי לפתוח את CodeNomad ממכשיר אחר.",
|
||||||
|
"remoteAccess.close": "סגור גישה מרוחקת",
|
||||||
|
"remoteAccess.refresh": "רענן",
|
||||||
|
|
||||||
|
"remoteAccess.sections.listeningMode.label": "מצב האזנה",
|
||||||
|
"remoteAccess.sections.listeningMode.help": "אפשר או הגבל גישה מרוחקת על ידי קישור לכל הממשקים או רק ל-localhost.",
|
||||||
|
"remoteAccess.toggle.on": "פועל",
|
||||||
|
"remoteAccess.toggle.off": "כבוי",
|
||||||
|
"remoteAccess.toggle.title": "אפשר חיבורים מכתובות IP אחרות",
|
||||||
|
"remoteAccess.toggle.caption.all": "מקושר ל-0.0.0.0",
|
||||||
|
"remoteAccess.toggle.caption.local": "מקושר ל-127.0.0.1",
|
||||||
|
"remoteAccess.toggle.note": "שינוי זה דורש הפעלה מחדש ועוצר זמנית את כל המופעים הפעילים. שתף את הכתובות למטה לאחר שהשרת יופעל מחדש.",
|
||||||
|
"remoteAccess.listeningMode.restartConfirm.message": "להפעיל מחדש כדי להחיל מצב האזנה? פעולה זו תעצור את כל המופעים הפעילים.",
|
||||||
|
"remoteAccess.listeningMode.restartConfirm.title.all": "פתוח למכשירים אחרים",
|
||||||
|
"remoteAccess.listeningMode.restartConfirm.title.local": "מוגבל למכשיר זה",
|
||||||
|
"remoteAccess.listeningMode.restartConfirm.confirmLabel": "הפעל מחדש עכשיו",
|
||||||
|
"remoteAccess.listeningMode.restartConfirm.cancelLabel": "ביטול",
|
||||||
|
"remoteAccess.restart.errorManual": "לא ניתן להפעיל מחדש אוטומטית. אנא הפעל מחדש את האפליקציה כדי להחיל את השינוי.",
|
||||||
|
|
||||||
|
"remoteAccess.sections.serverPassword.label": "סיסמת שרת",
|
||||||
|
"remoteAccess.sections.serverPassword.help": "גישה מרוחקת דורשת סיסמה. הגדר סיסמה קלה לזכירה כדי לאפשר כניסות ממכשירים אחרים.",
|
||||||
|
"remoteAccess.authStatus.unavailable": "סטטוס האימות אינו זמין.",
|
||||||
|
"remoteAccess.username": "שם משתמש: {username}",
|
||||||
|
"remoteAccess.password.status.set": "סיסמה מוגדרת לגישה מרוחקת.",
|
||||||
|
"remoteAccess.password.status.unset": "לא הוגדרה סיסמה קלה לזכירה. הגדר סיסמה כדי לאפשר כניסות גישה מרוחקת.",
|
||||||
|
"remoteAccess.password.actions.cancel": "ביטול",
|
||||||
|
"remoteAccess.password.actions.change": "שנה סיסמה",
|
||||||
|
"remoteAccess.password.actions.set": "הגדר סיסמה",
|
||||||
|
"remoteAccess.password.form.newPassword": "סיסמה חדשה",
|
||||||
|
"remoteAccess.password.form.confirmPassword": "אשר סיסמה",
|
||||||
|
"remoteAccess.password.form.placeholder": "לפחות 8 תווים",
|
||||||
|
"remoteAccess.password.error.tooShort": "הסיסמה חייבת להכיל לפחות 8 תווים.",
|
||||||
|
"remoteAccess.password.error.mismatch": "הסיסמאות אינן תואמות.",
|
||||||
|
"remoteAccess.password.save.saving": "שומר…",
|
||||||
|
"remoteAccess.password.save.label": "שמור סיסמה",
|
||||||
|
|
||||||
|
"remoteAccess.sections.addresses.label": "כתובות נגישות",
|
||||||
|
"remoteAccess.sections.addresses.help": "הפעל או סרוק ממכונה אחרת להעברת שליטה.",
|
||||||
|
"remoteAccess.addresses.loading": "טוען כתובות…",
|
||||||
|
"remoteAccess.addresses.none": "אין כתובות זמינות עדיין.",
|
||||||
|
"remoteAccess.address.scope.network": "רשת",
|
||||||
|
"remoteAccess.address.scope.loopback": "לולאה מקומית",
|
||||||
|
"remoteAccess.address.scope.internal": "פנימי",
|
||||||
|
"remoteAccess.address.open": "פתח",
|
||||||
|
"remoteAccess.address.showQr": "הצג QR",
|
||||||
|
"remoteAccess.address.hideQr": "הסתר QR",
|
||||||
|
"remoteAccess.address.qrAlt": "QR עבור {url}",
|
||||||
|
} as const
|
||||||
90
packages/ui/src/lib/i18n/messages/he/session.ts
Normal file
90
packages/ui/src/lib/i18n/messages/he/session.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export const sessionMessages = {
|
||||||
|
"sessionPicker.title": "OpenCode • {folder}",
|
||||||
|
"sessionPicker.empty.noPrevious": "אין סשנים קודמים",
|
||||||
|
"sessionPicker.resume.title": "המשך סשן ({count}):",
|
||||||
|
"sessionPicker.session.untitled": "ללא שם",
|
||||||
|
"sessionPicker.divider.or": "או",
|
||||||
|
"sessionPicker.new.title": "התחל סשן חדש:",
|
||||||
|
"sessionPicker.agents.loading": "טוען סוכנים...",
|
||||||
|
"sessionPicker.actions.creating": "יוצר...",
|
||||||
|
"sessionPicker.actions.createSession": "צור סשן",
|
||||||
|
"sessionPicker.actions.cancel": "ביטול",
|
||||||
|
|
||||||
|
"sessionList.header.title": "סשנים",
|
||||||
|
"sessionList.session.untitled": "ללא שם",
|
||||||
|
"sessionList.status.working": "עובד",
|
||||||
|
"sessionList.status.compacting": "מסכם",
|
||||||
|
"sessionList.status.idle": "מוכן",
|
||||||
|
"sessionList.status.needsPermission": "נדרש אישור",
|
||||||
|
"sessionList.status.needsInput": "נדרש קלט",
|
||||||
|
"sessionList.expand.collapseAriaLabel": "כווץ סשן",
|
||||||
|
"sessionList.expand.expandAriaLabel": "פרוס סשן",
|
||||||
|
"sessionList.expand.collapseTitle": "כווץ",
|
||||||
|
"sessionList.expand.expandTitle": "פרוס",
|
||||||
|
"sessionList.actions.newSession.ariaLabel": "סשן חדש",
|
||||||
|
"sessionList.actions.newSession.title": "סשן חדש",
|
||||||
|
"sessionList.actions.copyId.ariaLabel": "העתק מזהה סשן",
|
||||||
|
"sessionList.actions.copyId.title": "העתק מזהה סשן",
|
||||||
|
"sessionList.actions.rename.ariaLabel": "שנה שם סשן",
|
||||||
|
"sessionList.actions.rename.title": "שנה שם סשן",
|
||||||
|
"sessionList.actions.delete.ariaLabel": "מחק סשן",
|
||||||
|
"sessionList.actions.delete.title": "מחק סשן",
|
||||||
|
"sessionList.copyId.success": "מזהה סשן הועתק",
|
||||||
|
"sessionList.copyId.error": "לא ניתן להעתיק מזהה סשן",
|
||||||
|
"sessionList.delete.error": "לא ניתן למחוק סשן",
|
||||||
|
"sessionList.delete.title": "מחק סשן",
|
||||||
|
"sessionList.delete.confirmMessage": "למחוק את \"{label}\"? לא ניתן לבטל פעולה זו.",
|
||||||
|
"sessionList.delete.confirmLabel": "מחק",
|
||||||
|
"sessionList.delete.cancelLabel": "ביטול",
|
||||||
|
"sessionList.rename.error": "לא ניתן לשנות שם הסשן",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "חפש סשנים…",
|
||||||
|
"sessionList.filter.ariaLabel": "חפש סשנים",
|
||||||
|
"sessionList.selection.selectAllLabel": "בחר הכל",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "בחר את כל הסשנים",
|
||||||
|
"sessionList.selection.clearLabel": "נקה",
|
||||||
|
"sessionList.selection.clearAriaLabel": "נקה בחירה",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "בחר סשן",
|
||||||
|
"sessionList.bulkDelete.button": "מחק {count}",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "מחק {count} סשנים נבחרים",
|
||||||
|
"sessionList.bulkDelete.title": "מחק סשנים",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "למחוק {count} סשנים נבחרים? לא ניתן לבטל פעולה זו.",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "מחק",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "ביטול",
|
||||||
|
"sessionList.bulkDelete.error": "לא ניתן למחוק {count} סשנים",
|
||||||
|
|
||||||
|
"sessionRenameDialog.title": "שנה שם סשן",
|
||||||
|
"sessionRenameDialog.description.withLabel": "עדכן את הכותרת עבור \"{label}\".",
|
||||||
|
"sessionRenameDialog.description.default": "הגדר כותרת חדשה לסשן זה.",
|
||||||
|
"sessionRenameDialog.input.label": "שם סשן",
|
||||||
|
"sessionRenameDialog.input.placeholder": "הזן שם סשן",
|
||||||
|
"sessionRenameDialog.actions.cancel": "ביטול",
|
||||||
|
"sessionRenameDialog.actions.rename": "שנה שם",
|
||||||
|
"sessionRenameDialog.actions.renaming": "משנה שם…",
|
||||||
|
|
||||||
|
"sessionView.fallback.sessionNotFound": "הסשן לא נמצא",
|
||||||
|
"sessionView.alerts.abortFailed.message": "עצירת הסשן נכשלה",
|
||||||
|
"sessionView.alerts.abortFailed.title": "העצירה נכשלה",
|
||||||
|
"sessionView.alerts.revertFailed.message": "החזרה להודעה נכשלה",
|
||||||
|
"sessionView.alerts.revertFailed.title": "החזרה נכשלה",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "מחיקת הודעות נכשלה",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "המחיקה נכשלה",
|
||||||
|
"sessionView.alerts.forkFailed.message": "פיצול הסשן נכשל",
|
||||||
|
"sessionView.alerts.forkFailed.title": "הפיצול נכשל",
|
||||||
|
"sessionView.attachments.expandPastedTextAriaLabel": "פרוס טקסט שהודבק",
|
||||||
|
"sessionView.attachments.insertPastedTextTitle": "הכנס טקסט שהודבק",
|
||||||
|
"sessionView.attachments.removeAriaLabel": "הסר קובץ מצורף",
|
||||||
|
|
||||||
|
"sessionEvents.sessionCompactedToast": "הסשן {label} סוכם",
|
||||||
|
"sessionEvents.sessionError.unknown": "שגיאה לא ידועה",
|
||||||
|
"sessionEvents.sessionError.title": "שגיאת סשן",
|
||||||
|
"sessionEvents.sessionError.message": "שגיאה: {message}",
|
||||||
|
|
||||||
|
"sessionState.cleanup.deepConfirm.message": "ניקוי עמוק זה עשוי להיות איטי, ועלול למחוק סשנים שלא התכוונת למחוק. האם אתה בטוח?",
|
||||||
|
"sessionState.cleanup.deepConfirm.title": "ניקוי עמוק של סשנים",
|
||||||
|
"sessionState.cleanup.deepConfirm.detail": "ניקוי עמוק של סשנים ימחק את כל הסשנים ללא הודעות, יסיר סשני תת-סוכן שסיימו, וינקה פיצולים לא בשימוש של סשן.",
|
||||||
|
"sessionState.cleanup.deepConfirm.confirmLabel": "המשך",
|
||||||
|
"sessionState.cleanup.deepConfirm.cancelLabel": "ביטול",
|
||||||
|
"sessionState.cleanup.toast.one": "נוקה {count} סשן ריק",
|
||||||
|
"sessionState.cleanup.toast.other": "נוקו {count} סשנים ריקים",
|
||||||
|
} as const
|
||||||
142
packages/ui/src/lib/i18n/messages/he/settings.ts
Normal file
142
packages/ui/src/lib/i18n/messages/he/settings.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
export const settingsMessages = {
|
||||||
|
"instanceServiceStatus.sections.lsp": "שרתי LSP",
|
||||||
|
"instanceServiceStatus.sections.mcp": "שרתי MCP",
|
||||||
|
"instanceServiceStatus.sections.plugins": "תוספים",
|
||||||
|
"instanceServiceStatus.lsp.loading": "טוען שרתי LSP...",
|
||||||
|
"instanceServiceStatus.lsp.empty": "לא זוהו שרתי LSP.",
|
||||||
|
"instanceServiceStatus.lsp.status.connected": "מחובר",
|
||||||
|
"instanceServiceStatus.lsp.status.error": "שגיאה",
|
||||||
|
"instanceServiceStatus.mcp.loading": "טוען שרתי MCP...",
|
||||||
|
"instanceServiceStatus.mcp.empty": "לא זוהו שרתי MCP.",
|
||||||
|
"instanceServiceStatus.mcp.toggleAriaLabel": "הפעל/כבה שרת MCP {name}",
|
||||||
|
"instanceServiceStatus.plugins.loading": "טוען תוספים...",
|
||||||
|
"instanceServiceStatus.plugins.empty": "לא הוגדרו תוספים.",
|
||||||
|
|
||||||
|
"permissionBanner.pendingRequests.one": "בקשה אחת ממתינה",
|
||||||
|
"permissionBanner.pendingRequests.other": "{count} בקשות ממתינות",
|
||||||
|
"permissionBanner.detail.permission.one": "אישור אחד",
|
||||||
|
"permissionBanner.detail.permission.other": "{count} אישורים",
|
||||||
|
"permissionBanner.detail.question.one": "שאלה אחת",
|
||||||
|
"permissionBanner.detail.question.other": "{count} שאלות",
|
||||||
|
"permissionBanner.detail.wrapper": " ({detail})",
|
||||||
|
|
||||||
|
"agentSelector.placeholder": "בחר סוכן...",
|
||||||
|
"agentSelector.badge.subagent": "תת-סוכן",
|
||||||
|
"agentSelector.none": "ללא",
|
||||||
|
"agentSelector.trigger.primary": "סוכן: {agent}",
|
||||||
|
|
||||||
|
"modelSelector.placeholder.search": "חפש מודלים...",
|
||||||
|
"modelSelector.none": "ללא",
|
||||||
|
"modelSelector.trigger.primary": "מודל: {model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "הצג מועדפים בלבד",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "הצג את כל המודלים",
|
||||||
|
"modelSelector.favorite.add": "הוסף למועדפים",
|
||||||
|
"modelSelector.favorite.remove": "הסר ממועדפים",
|
||||||
|
|
||||||
|
"thinkingSelector.variant.default": "ברירת מחדל",
|
||||||
|
"thinkingSelector.label": "חשיבה: {variant}",
|
||||||
|
|
||||||
|
"envEditor.title": "משתני סביבה",
|
||||||
|
"envEditor.count.one": "(משתנה אחד)",
|
||||||
|
"envEditor.count.other": "({count} משתנים)",
|
||||||
|
"envEditor.fields.name.placeholder": "שם משתנה",
|
||||||
|
"envEditor.fields.name.readOnlyTitle": "שם משתנה (לקריאה בלבד)",
|
||||||
|
"envEditor.fields.value.placeholder": "ערך משתנה",
|
||||||
|
"envEditor.actions.remove.title": "הסר משתנה",
|
||||||
|
"envEditor.actions.add.title": "הוסף משתנה",
|
||||||
|
"envEditor.empty": "לא הוגדרו משתני סביבה. הוסף משתנים למעלה להתאמת סביבת OpenCode.",
|
||||||
|
"envEditor.help": "משתנים אלו יהיו זמינים בסביבת OpenCode בעת הפעלת מופעים.",
|
||||||
|
|
||||||
|
"contextUsagePanel.headings.tokens": "טוקנים",
|
||||||
|
"contextUsagePanel.headings.context": "הקשר",
|
||||||
|
"contextUsagePanel.labels.input": "קלט",
|
||||||
|
"contextUsagePanel.labels.output": "פלט",
|
||||||
|
"contextUsagePanel.labels.cost": "עלות",
|
||||||
|
"contextUsagePanel.labels.used": "בשימוש",
|
||||||
|
"contextUsagePanel.labels.available": "זמין",
|
||||||
|
"contextUsagePanel.unavailable": "--",
|
||||||
|
|
||||||
|
"settings.title": "הגדרות",
|
||||||
|
"settings.navigationAriaLabel": "קטגוריות הגדרות",
|
||||||
|
"settings.close": "סגור הגדרות",
|
||||||
|
"settings.content.eyebrow": "העדפות סביבת עבודה",
|
||||||
|
"settings.open.title": "פתח הגדרות",
|
||||||
|
"settings.open.ariaLabel": "פתח הגדרות",
|
||||||
|
"settings.nav.appearance": "מראה",
|
||||||
|
"settings.nav.notifications": "התראות",
|
||||||
|
"settings.nav.remote": "גישה מרוחקת",
|
||||||
|
"settings.nav.opencode": "OpenCode",
|
||||||
|
"settings.scope.device": "מכשיר זה",
|
||||||
|
"settings.scope.server": "הגדרת שרת",
|
||||||
|
"settings.common.enabled": "מופעל",
|
||||||
|
"settings.common.disabled": "מושבת",
|
||||||
|
"settings.section.appearance.title": "מראה",
|
||||||
|
"settings.section.appearance.subtitle": "שנה כיצד האפליקציה נראית במכשיר זה.",
|
||||||
|
"settings.appearance.theme.title": "ערכת נושא",
|
||||||
|
"settings.appearance.theme.subtitle": "בחר את מצב הצבע שישמש בכל האפליקציה.",
|
||||||
|
"settings.appearance.theme.option.system": "התאם להגדרת מערכת ההפעלה",
|
||||||
|
"settings.appearance.theme.option.light": "השתמש במראה בהיר",
|
||||||
|
"settings.appearance.theme.option.dark": "השתמש במראה כהה",
|
||||||
|
"settings.section.notifications.title": "התראות",
|
||||||
|
"settings.section.notifications.subtitle": "שלוט בהתראות ברמת מערכת ההפעלה עבור פעילות סשן.",
|
||||||
|
"settings.notifications.permission.granted": "ניתן",
|
||||||
|
"settings.notifications.permission.denied": "נדחה",
|
||||||
|
"settings.notifications.permission.default": "לא ניתן",
|
||||||
|
"settings.notifications.permission.unsupported": "לא נתמך",
|
||||||
|
"settings.notifications.messages.unsupportedEnvironment": "התראות מערכת ההפעלה אינן נתמכות בסביבה זו.",
|
||||||
|
"settings.notifications.messages.permissionDenied": "הרשאת התראות נדחתה. הפעל התראות בהגדרות המערכת או הדפדפן.",
|
||||||
|
"settings.notifications.messages.permissionNotGranted": "הרשאת התראות לא ניתנה.",
|
||||||
|
"settings.notifications.messages.unsupportedGeneral": "התראות אינן נתמכות בסביבה זו.",
|
||||||
|
"settings.notifications.messages.permissionGranted": "ההרשאה ניתנה. כעת ניתן להפעיל התראות.",
|
||||||
|
"settings.notifications.messages.permissionRequestDenied": "ההרשאה נדחתה. ייתכן שתצטרך להפעיל התראות בהגדרות המערכת או הדפדפן.",
|
||||||
|
"settings.notifications.sessionStatus.title": "התראות סטטוס סשן",
|
||||||
|
"settings.notifications.sessionStatus.subtitle": "קבל התראות כאשר סשנים דורשים את תשומת לבך.",
|
||||||
|
"settings.notifications.enable.title": "הפעל התראות",
|
||||||
|
"settings.notifications.enable.permission": "הרשאה: {permission}",
|
||||||
|
"settings.notifications.requestPermission.title": "בקש הרשאה",
|
||||||
|
"settings.notifications.requestPermission.subtitle": "אפשר לאפליקציה לשלוח התראות במכשיר זה.",
|
||||||
|
"settings.notifications.requestPermission.action": "בקש",
|
||||||
|
"settings.notifications.allowVisible.title": "התרע כאשר האפליקציה ממוקדת",
|
||||||
|
"settings.notifications.allowVisible.subtitle": "שמור על התראות פעילות גם כאשר חלון זה גלוי.",
|
||||||
|
"settings.notifications.unsupportedNote": "התראות אינן נתמכות בסביבה זו. פקד ההתראות נשאר מושבת.",
|
||||||
|
"settings.notifications.events.title": "התרע אותי כאשר",
|
||||||
|
"settings.notifications.events.subtitle": "בחר אילו אירועי סשן ישלחו התראות.",
|
||||||
|
"settings.notifications.events.needsInput": "הסשן דורש קלט",
|
||||||
|
"settings.notifications.events.idle": "הסשן עובר למצב סרלה",
|
||||||
|
"settings.notifications.status.enabled": "התראות מופעלות",
|
||||||
|
"settings.notifications.status.disabled": "התראות מושבתות",
|
||||||
|
"settings.notifications.status.unsupported": "התראות לא נתמכות",
|
||||||
|
"settings.section.remote.title": "גישה מרוחקת",
|
||||||
|
"settings.section.remote.subtitle": "בדוק כיצד שרת זה חשוף ברשת שלך ואבטח אישורי גישה.",
|
||||||
|
"settings.section.opencode.title": "OpenCode",
|
||||||
|
"settings.section.opencode.subtitle": "בחר את הקובץ הבינארי של OpenCode והסביבה לשימוש במופעים חדשים.",
|
||||||
|
"settings.opencode.runtime.title": "סביבת ריצה",
|
||||||
|
"settings.opencode.runtime.subtitle": "הגדר עם איזה קובץ בינארי של OpenCode מופעים חדשים יופעלו.",
|
||||||
|
|
||||||
|
"settings.appearance.behavior.title": "אינטראקציה",
|
||||||
|
"settings.appearance.behavior.subtitle": "ברירות מחדל להודעות, diff וקלט.",
|
||||||
|
"settings.behavior.keyboardHints.title": "רמזי קיצורי מקלדת",
|
||||||
|
"settings.behavior.keyboardHints.subtitle": "הצג רמזי קיצורי מקלדת בכל הממשק.",
|
||||||
|
"settings.behavior.thinking.title": "קטעי חשיבה",
|
||||||
|
"settings.behavior.thinking.subtitle": "הצג או הסתר קטעי חשיבה של ה-AI בהודעות.",
|
||||||
|
"settings.behavior.thinkingDefault.title": "ברירת מחדל לחשיבה",
|
||||||
|
"settings.behavior.thinkingDefault.subtitle": "בחר האם קטעי חשיבה מתחילים פרוסים או מכווצים.",
|
||||||
|
"settings.behavior.timelineTools.title": "קריאות כלי בציר הזמן",
|
||||||
|
"settings.behavior.timelineTools.subtitle": "הצג או הסתר קריאות כלי בציר הודעות.",
|
||||||
|
"settings.behavior.diffView.title": "תצוגת diff",
|
||||||
|
"settings.behavior.diffView.subtitle": "בחר כיצד מוצגים diff של קריאות כלי.",
|
||||||
|
"settings.behavior.diffView.option.split": "מפוצל",
|
||||||
|
"settings.behavior.diffView.option.unified": "מאוחד",
|
||||||
|
"settings.behavior.toolOutputsDefault.title": "ברירת מחדל לפלטי כלים",
|
||||||
|
"settings.behavior.toolOutputsDefault.subtitle": "בחר האם פלטי כלים מתחילים פרוסים או מכווצים.",
|
||||||
|
"settings.behavior.diagnosticsDefault.title": "ברירת מחדל לאבחון",
|
||||||
|
"settings.behavior.diagnosticsDefault.subtitle": "בחר האם פלט אבחון מתחיל פרוס או מכווץ.",
|
||||||
|
"settings.behavior.toolInputsVisibility.title": "נראות קלטי כלים",
|
||||||
|
"settings.behavior.toolInputsVisibility.subtitle": "הגדר נראות ברירת מחדל לארגומנטים של קריאות כלי.",
|
||||||
|
"settings.behavior.usageMetrics.title": "מדדי שימוש בטוקנים",
|
||||||
|
"settings.behavior.usageMetrics.subtitle": "הצג או הסתר נתוני טוקנים ועלות להודעות הסוכן.",
|
||||||
|
"settings.behavior.autoCleanup.title": "ניקוי אוטומטי של סשנים ריקים",
|
||||||
|
"settings.behavior.autoCleanup.subtitle": "נקה אוטומטית סשנים ריקים בעת יצירת סשנים חדשים.",
|
||||||
|
"settings.behavior.promptSubmit.title": "Enter לשליחה",
|
||||||
|
"settings.behavior.promptSubmit.subtitle": "השתמש ב-Enter לשליחת פקודות; Cmd/Ctrl+Enter מוסיף שורה חדשה.",
|
||||||
|
} as const
|
||||||
6
packages/ui/src/lib/i18n/messages/he/time.ts
Normal file
6
packages/ui/src/lib/i18n/messages/he/time.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const timeMessages = {
|
||||||
|
"time.relative.justNow": "עכשיו",
|
||||||
|
"time.relative.daysAgoShort": "לפני {count} ימים",
|
||||||
|
"time.relative.hoursAgoShort": "לפני {count} שעות",
|
||||||
|
"time.relative.minutesAgoShort": "לפני {count} דקות",
|
||||||
|
} as const
|
||||||
132
packages/ui/src/lib/i18n/messages/he/toolCall.ts
Normal file
132
packages/ui/src/lib/i18n/messages/he/toolCall.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
export const toolCallMessages = {
|
||||||
|
"toolCall.pending.waitingToRun": "ממתין להרצה...",
|
||||||
|
"toolCall.error.label": "שגיאה:",
|
||||||
|
|
||||||
|
"toolCall.header.copyTitle": "העתק כותרת קריאת כלי",
|
||||||
|
"toolCall.header.copyAriaLabel": "העתק כותרת קריאת כלי",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "הצג ארגומנטי כלי",
|
||||||
|
"toolCall.header.showInputAriaLabel": "הצג ארגומנטי כלי",
|
||||||
|
"toolCall.header.hideInputTitle": "הסתר ארגומנטי כלי",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "הסתר ארגומנטי כלי",
|
||||||
|
|
||||||
|
"toolCall.io.input": "קלט כלי",
|
||||||
|
"toolCall.io.output": "פלט כלי",
|
||||||
|
|
||||||
|
"toolCall.diff.label": "Diff",
|
||||||
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
|
"toolCall.diff.viewMode.ariaLabel": "מצב תצוגת diff",
|
||||||
|
"toolCall.diff.viewMode.split": "מפוצל",
|
||||||
|
"toolCall.diff.viewMode.unified": "מאוחד",
|
||||||
|
|
||||||
|
"toolCall.diagnostics.title": "אבחון",
|
||||||
|
"toolCall.diagnostics.ariaLabel": "אבחון",
|
||||||
|
"toolCall.diagnostics.ariaLabel.withLabel": "אבחון {label}",
|
||||||
|
"toolCall.diagnostics.severity.error.short": "שגיאה",
|
||||||
|
"toolCall.diagnostics.severity.warning.short": "אזהרה",
|
||||||
|
"toolCall.diagnostics.severity.info.short": "מידע",
|
||||||
|
|
||||||
|
"toolCall.renderer.toolName.shell": "מעטפת",
|
||||||
|
"toolCall.renderer.toolName.fetch": "Fetch",
|
||||||
|
"toolCall.renderer.toolName.invalid": "לא תקין",
|
||||||
|
"toolCall.renderer.toolName.plan": "תוכנית",
|
||||||
|
"toolCall.renderer.toolName.applyPatch": "החל תיקון",
|
||||||
|
|
||||||
|
"toolCall.renderer.action.working": "עובד...",
|
||||||
|
"toolCall.renderer.action.writingCommand": "כותב פקודה...",
|
||||||
|
"toolCall.renderer.action.preparingEdit": "מכין עריכה...",
|
||||||
|
"toolCall.renderer.action.readingFile": "קורא קובץ...",
|
||||||
|
"toolCall.renderer.action.preparingWrite": "מכין כתיבה...",
|
||||||
|
"toolCall.renderer.action.preparingPatch": "מכין תיקון...",
|
||||||
|
"toolCall.renderer.action.planning": "מתכנן...",
|
||||||
|
"toolCall.renderer.action.fetchingFromWeb": "מאחזר מהאינטרנט...",
|
||||||
|
"toolCall.renderer.action.findingFiles": "מחפש קבצים...",
|
||||||
|
"toolCall.renderer.action.searchingContent": "מחפש תוכן...",
|
||||||
|
"toolCall.renderer.action.listingDirectory": "מפרט ספרייה...",
|
||||||
|
|
||||||
|
"toolCall.renderer.bash.title.timeout": "פסק זמן: {timeout}",
|
||||||
|
"toolCall.renderer.read.detail.offset": "היסט: {offset}",
|
||||||
|
"toolCall.renderer.read.detail.limit": "מגבלה: {limit}",
|
||||||
|
|
||||||
|
"toolCall.renderer.todo.empty": "אין פריטי תוכנית עדיין.",
|
||||||
|
"toolCall.renderer.todo.status.pending": "ממתין",
|
||||||
|
"toolCall.renderer.todo.status.inProgress": "בביצוע",
|
||||||
|
"toolCall.renderer.todo.status.completed": "הושלם",
|
||||||
|
"toolCall.renderer.todo.status.cancelled": "בוטל",
|
||||||
|
"toolCall.renderer.todo.title.plan": "תוכנית",
|
||||||
|
"toolCall.renderer.todo.title.creating": "יוצר תוכנית",
|
||||||
|
"toolCall.renderer.todo.title.completing": "משלים תוכנית",
|
||||||
|
"toolCall.renderer.todo.title.updating": "מעדכן תוכנית",
|
||||||
|
|
||||||
|
"toolCall.permission.status.required": "נדרש אישור",
|
||||||
|
"toolCall.permission.status.queued": "אישור בתור",
|
||||||
|
"toolCall.permission.requestedDiff.label": "diff מבוקש",
|
||||||
|
"toolCall.permission.requestedDiff.withPath": "diff מבוקש · {path}",
|
||||||
|
"toolCall.permission.queuedText": "ממתין לתגובות אישור קודמות.",
|
||||||
|
"toolCall.permission.actions.allowOnce": "אפשר פעם אחת",
|
||||||
|
"toolCall.permission.actions.alwaysAllow": "אפשר תמיד",
|
||||||
|
"toolCall.permission.actions.deny": "דחה",
|
||||||
|
"toolCall.permission.shortcuts.allowOnce": "אפשר פעם אחת",
|
||||||
|
"toolCall.permission.shortcuts.alwaysAllow": "אפשר תמיד",
|
||||||
|
"toolCall.permission.shortcuts.deny": "דחה",
|
||||||
|
"toolCall.permission.errors.unableToUpdate": "לא ניתן לעדכן אישור",
|
||||||
|
|
||||||
|
"permissionApproval.title": "בקשות",
|
||||||
|
"permissionApproval.empty": "אין בקשות ממתינות.",
|
||||||
|
"permissionApproval.kind.permission": "אישור",
|
||||||
|
"permissionApproval.kind.question": "שאלה",
|
||||||
|
"permissionApproval.questionCount.one": "שאלה אחת",
|
||||||
|
"permissionApproval.questionCount.other": "{count} שאלות",
|
||||||
|
"permissionApproval.status.active": "פעיל",
|
||||||
|
"permissionApproval.actions.closeAriaLabel": "סגור",
|
||||||
|
"permissionApproval.actions.goToSession": "עבור לסשן",
|
||||||
|
"permissionApproval.actions.loadingSession": "טוען…",
|
||||||
|
"permissionApproval.actions.loadSession": "טען סשן",
|
||||||
|
"permissionApproval.actions.allowOnce": "אפשר פעם אחת",
|
||||||
|
"permissionApproval.actions.alwaysAllow": "אפשר תמיד",
|
||||||
|
"permissionApproval.actions.deny": "דחה",
|
||||||
|
"permissionApproval.fallbackHint": "טען סשן לקבלת מידע נוסף.",
|
||||||
|
"permissionApproval.errors.unableToUpdatePermission": "לא ניתן לעדכן אישור",
|
||||||
|
|
||||||
|
"toolCall.question.status.required": "נדרשת תשובה",
|
||||||
|
"toolCall.question.status.queued": "שאלה בתור",
|
||||||
|
"toolCall.question.status.questions": "שאלות",
|
||||||
|
"toolCall.question.action.awaitingAnswers": "ממתין לתשובות...",
|
||||||
|
"toolCall.question.title.questions": "שאלות",
|
||||||
|
"toolCall.question.title.askingQuestions": "שואל שאלות",
|
||||||
|
"toolCall.question.type.one": "שאלה",
|
||||||
|
"toolCall.question.type.other": "שאלות",
|
||||||
|
"toolCall.question.number": "ש{number}:",
|
||||||
|
"toolCall.question.multiple": "מרובות",
|
||||||
|
"toolCall.question.custom.title": "הקלד תשובה מותאמת אישית",
|
||||||
|
"toolCall.question.custom.label": "תשובה מותאמת אישית",
|
||||||
|
"toolCall.question.custom.placeholder": "הקלד תשובה משלך",
|
||||||
|
"toolCall.question.actions.submit": "שלח",
|
||||||
|
"toolCall.question.actions.dismiss": "סגור",
|
||||||
|
"toolCall.question.shortcuts.submit": "שלח",
|
||||||
|
"toolCall.question.shortcuts.dismiss": "סגור",
|
||||||
|
"toolCall.question.queuedText": "ממתין לתגובות קודמות.",
|
||||||
|
"toolCall.question.validation.answerAll": "אנא ענה על כל השאלות לפני השליחה.",
|
||||||
|
"toolCall.question.errors.unableToReply": "לא ניתן לשלוח תשובה",
|
||||||
|
"toolCall.question.errors.unableToDismiss": "לא ניתן לסגור",
|
||||||
|
|
||||||
|
"toolCall.task.action.delegating": "מאציל...",
|
||||||
|
"toolCall.task.sections.prompt": "פקודה",
|
||||||
|
"toolCall.task.sections.steps": "שלבים",
|
||||||
|
"toolCall.task.sections.output": "פלט",
|
||||||
|
"toolCall.task.steps.count": "{count} שלבים",
|
||||||
|
"toolCall.task.meta.agentModel": "סוכן: {agent} • מודל: {model}",
|
||||||
|
"toolCall.task.meta.agent": "סוכן: {agent}",
|
||||||
|
"toolCall.task.meta.model": "מודל: {model}",
|
||||||
|
|
||||||
|
"toolCall.status.pending": "ממתין",
|
||||||
|
"toolCall.status.running": "רץ",
|
||||||
|
"toolCall.status.completed": "הושלם",
|
||||||
|
"toolCall.status.error": "שגיאה",
|
||||||
|
"toolCall.status.unknown": "לא ידוע",
|
||||||
|
|
||||||
|
"toolCall.applyPatch.action.preparing": "מכין apply_patch...",
|
||||||
|
"toolCall.applyPatch.title.withFileCount.one": "{tool} (קובץ אחד)",
|
||||||
|
"toolCall.applyPatch.title.withFileCount.other": "{tool} ({count} קבצים)",
|
||||||
|
"toolCall.applyPatch.fileFallback": "קובץ {number}",
|
||||||
|
} as const
|
||||||
@@ -90,6 +90,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.rightPanel.title": "ステータスパネル",
|
"instanceShell.rightPanel.title": "ステータスパネル",
|
||||||
"instanceShell.rightPanel.tabs.changes": "変更",
|
"instanceShell.rightPanel.tabs.changes": "変更",
|
||||||
|
"instanceShell.rightPanel.tabs.gitChanges": "Git 変更",
|
||||||
"instanceShell.rightPanel.tabs.files": "ファイル",
|
"instanceShell.rightPanel.tabs.files": "ファイル",
|
||||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||||
@@ -112,6 +113,10 @@ export const instanceMessages = {
|
|||||||
"instanceShell.sessionChanges.filesChanged": "{count} 個のファイルが変更されました",
|
"instanceShell.sessionChanges.filesChanged": "{count} 個のファイルが変更されました",
|
||||||
"instanceShell.sessionChanges.actions.show": "変更を表示",
|
"instanceShell.sessionChanges.actions.show": "変更を表示",
|
||||||
|
|
||||||
|
"instanceShell.gitChanges.loading": "Git の変更を読み込み中...",
|
||||||
|
"instanceShell.gitChanges.empty": "Git の変更はまだありません。",
|
||||||
|
"instanceShell.gitChanges.deleted": "削除済み",
|
||||||
|
|
||||||
"instanceShell.filesShell.fileListTitle": "ファイル一覧",
|
"instanceShell.filesShell.fileListTitle": "ファイル一覧",
|
||||||
"instanceShell.filesShell.mobileSelectorLabel": "ファイルを選択",
|
"instanceShell.filesShell.mobileSelectorLabel": "ファイルを選択",
|
||||||
"instanceShell.filesShell.mobileSelectorEmpty": "ファイルを選択してください",
|
"instanceShell.filesShell.mobileSelectorEmpty": "ファイルを選択してください",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.rightPanel.title": "Панель состояния",
|
"instanceShell.rightPanel.title": "Панель состояния",
|
||||||
"instanceShell.rightPanel.tabs.changes": "Изменения",
|
"instanceShell.rightPanel.tabs.changes": "Изменения",
|
||||||
|
"instanceShell.rightPanel.tabs.gitChanges": "Изменения Git",
|
||||||
"instanceShell.rightPanel.tabs.files": "Файлы",
|
"instanceShell.rightPanel.tabs.files": "Файлы",
|
||||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||||
@@ -112,6 +113,10 @@ export const instanceMessages = {
|
|||||||
"instanceShell.sessionChanges.filesChanged": "Изменено файлов: {count}",
|
"instanceShell.sessionChanges.filesChanged": "Изменено файлов: {count}",
|
||||||
"instanceShell.sessionChanges.actions.show": "Показать изменения",
|
"instanceShell.sessionChanges.actions.show": "Показать изменения",
|
||||||
|
|
||||||
|
"instanceShell.gitChanges.loading": "Загрузка изменений Git...",
|
||||||
|
"instanceShell.gitChanges.empty": "Изменений Git пока нет.",
|
||||||
|
"instanceShell.gitChanges.deleted": "Удалено",
|
||||||
|
|
||||||
"instanceShell.filesShell.fileListTitle": "Список файлов",
|
"instanceShell.filesShell.fileListTitle": "Список файлов",
|
||||||
"instanceShell.filesShell.mobileSelectorLabel": "Выбрать файл",
|
"instanceShell.filesShell.mobileSelectorLabel": "Выбрать файл",
|
||||||
"instanceShell.filesShell.mobileSelectorEmpty": "Выберите файл",
|
"instanceShell.filesShell.mobileSelectorEmpty": "Выберите файл",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.rightPanel.title": "状态面板",
|
"instanceShell.rightPanel.title": "状态面板",
|
||||||
"instanceShell.rightPanel.tabs.changes": "更改",
|
"instanceShell.rightPanel.tabs.changes": "更改",
|
||||||
|
"instanceShell.rightPanel.tabs.gitChanges": "Git 更改",
|
||||||
"instanceShell.rightPanel.tabs.files": "文件",
|
"instanceShell.rightPanel.tabs.files": "文件",
|
||||||
"instanceShell.rightPanel.tabs.status": "状态",
|
"instanceShell.rightPanel.tabs.status": "状态",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||||
@@ -112,6 +113,10 @@ export const instanceMessages = {
|
|||||||
"instanceShell.sessionChanges.filesChanged": "已更改 {count} 个文件",
|
"instanceShell.sessionChanges.filesChanged": "已更改 {count} 个文件",
|
||||||
"instanceShell.sessionChanges.actions.show": "显示更改",
|
"instanceShell.sessionChanges.actions.show": "显示更改",
|
||||||
|
|
||||||
|
"instanceShell.gitChanges.loading": "正在加载 Git 更改...",
|
||||||
|
"instanceShell.gitChanges.empty": "暂无 Git 更改。",
|
||||||
|
"instanceShell.gitChanges.deleted": "已删除",
|
||||||
|
|
||||||
"instanceShell.filesShell.fileListTitle": "文件列表",
|
"instanceShell.filesShell.fileListTitle": "文件列表",
|
||||||
"instanceShell.filesShell.mobileSelectorLabel": "选择文件",
|
"instanceShell.filesShell.mobileSelectorLabel": "选择文件",
|
||||||
"instanceShell.filesShell.mobileSelectorEmpty": "请选择文件",
|
"instanceShell.filesShell.mobileSelectorEmpty": "请选择文件",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { marked } from "marked"
|
import { marked } from "marked"
|
||||||
import { createHighlighter, type Highlighter, bundledLanguages } from "shiki/bundle/full"
|
|
||||||
import { getLogger } from "./logger"
|
import { getLogger } from "./logger"
|
||||||
import { tGlobal } from "./i18n"
|
import { tGlobal } from "./i18n"
|
||||||
|
import type { Highlighter } from "shiki/bundle/full"
|
||||||
|
import { decodeHtmlEntities, escapeHtml } from "./text-render-utils"
|
||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
@@ -11,43 +12,8 @@ let currentTheme: "light" | "dark" = "light"
|
|||||||
let isInitialized = false
|
let isInitialized = false
|
||||||
let highlightSuppressed = false
|
let highlightSuppressed = false
|
||||||
let rendererSetup = false
|
let rendererSetup = false
|
||||||
|
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
||||||
const extensionToLanguage: Record<string, string> = {
|
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
|
||||||
ts: "typescript",
|
|
||||||
tsx: "typescript",
|
|
||||||
js: "javascript",
|
|
||||||
jsx: "javascript",
|
|
||||||
py: "python",
|
|
||||||
sh: "bash",
|
|
||||||
bash: "bash",
|
|
||||||
json: "json",
|
|
||||||
html: "html",
|
|
||||||
css: "css",
|
|
||||||
md: "markdown",
|
|
||||||
yaml: "yaml",
|
|
||||||
yml: "yaml",
|
|
||||||
sql: "sql",
|
|
||||||
rs: "rust",
|
|
||||||
go: "go",
|
|
||||||
cpp: "cpp",
|
|
||||||
cc: "cpp",
|
|
||||||
cxx: "cpp",
|
|
||||||
hpp: "cpp",
|
|
||||||
h: "cpp",
|
|
||||||
c: "c",
|
|
||||||
java: "java",
|
|
||||||
cs: "csharp",
|
|
||||||
php: "php",
|
|
||||||
rb: "ruby",
|
|
||||||
swift: "swift",
|
|
||||||
kt: "kotlin",
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLanguageFromPath(path?: string | null): string | undefined {
|
|
||||||
if (!path) return undefined
|
|
||||||
const ext = path.split(".").pop()?.toLowerCase()
|
|
||||||
return ext ? extensionToLanguage[ext] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track loaded languages and queue for on-demand loading
|
// Track loaded languages and queue for on-demand loading
|
||||||
const loadedLanguages = new Set<string>()
|
const loadedLanguages = new Set<string>()
|
||||||
@@ -89,10 +55,15 @@ async function getOrCreateHighlighter() {
|
|||||||
return highlighterPromise
|
return highlighterPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create highlighter with no preloaded languages
|
highlighterPromise = (async () => {
|
||||||
highlighterPromise = createHighlighter({
|
const shiki = await loadShikiModule()
|
||||||
themes: ["github-light", "github-light-high-contrast", "github-dark"],
|
return shiki.createHighlighter({
|
||||||
langs: [],
|
themes: ["github-light", "github-light-high-contrast", "github-dark"],
|
||||||
|
langs: [],
|
||||||
|
})
|
||||||
|
})().catch((error) => {
|
||||||
|
highlighterPromise = null
|
||||||
|
throw error
|
||||||
})
|
})
|
||||||
|
|
||||||
highlighter = await highlighterPromise
|
highlighter = await highlighterPromise
|
||||||
@@ -100,12 +71,37 @@ async function getOrCreateHighlighter() {
|
|||||||
return highlighter
|
return highlighter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadShikiModule() {
|
||||||
|
if (!shikiModulePromise) {
|
||||||
|
shikiModulePromise = import("shiki/bundle/full").then((module) => {
|
||||||
|
bundledLanguagesCache = module.bundledLanguages
|
||||||
|
return module
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return shikiModulePromise
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueHighlighterWarmup() {
|
||||||
|
if (highlighter || highlighterPromise) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void getOrCreateHighlighter().catch((error) => {
|
||||||
|
log.warn("Failed to initialize markdown highlighter", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeLanguageToken(token: string): string {
|
function normalizeLanguageToken(token: string): string {
|
||||||
return token.trim().toLowerCase()
|
return token.trim().toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLanguage(token: string): { canonical: string | null; raw: string } {
|
function resolveLanguage(token: string): { canonical: string | null; raw: string } {
|
||||||
const normalized = normalizeLanguageToken(token)
|
const normalized = normalizeLanguageToken(token)
|
||||||
|
const bundledLanguages = bundledLanguagesCache
|
||||||
|
if (!bundledLanguages) {
|
||||||
|
return { canonical: null, raw: normalized }
|
||||||
|
}
|
||||||
|
|
||||||
// Check if it's a direct key match
|
// Check if it's a direct key match
|
||||||
if (normalized in bundledLanguages) {
|
if (normalized in bundledLanguages) {
|
||||||
@@ -148,32 +144,43 @@ async function ensureLanguages(content: string) {
|
|||||||
|
|
||||||
// Queue language loading tasks
|
// Queue language loading tasks
|
||||||
for (const token of foundLanguages) {
|
for (const token of foundLanguages) {
|
||||||
const { canonical, raw } = resolveLanguage(token)
|
const rawToken = normalizeLanguageToken(token)
|
||||||
const langKey = canonical || raw
|
if (!rawToken) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Skip "text" and aliases since Shiki handles plain text already
|
// Skip "text" and aliases since Shiki handles plain text already
|
||||||
if (langKey === "text" || raw === "text") {
|
if (rawToken === "text") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if already loaded or queued
|
// Skip if already loaded or queued
|
||||||
if (loadedLanguages.has(langKey) || queuedLanguages.has(langKey)) {
|
if (loadedLanguages.has(rawToken) || queuedLanguages.has(rawToken)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
queuedLanguages.add(langKey)
|
queuedLanguages.add(rawToken)
|
||||||
|
|
||||||
// Queue the language loading task
|
// Queue the language loading task
|
||||||
languageLoadQueue.push(async () => {
|
languageLoadQueue.push(async () => {
|
||||||
try {
|
try {
|
||||||
|
await loadShikiModule()
|
||||||
|
const { canonical, raw } = resolveLanguage(token)
|
||||||
|
const langKey = canonical || raw
|
||||||
|
|
||||||
|
if (langKey === "text" || raw === "text") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const h = await getOrCreateHighlighter()
|
const h = await getOrCreateHighlighter()
|
||||||
await h.loadLanguage(langKey as never)
|
await h.loadLanguage(langKey as never)
|
||||||
loadedLanguages.add(langKey)
|
loadedLanguages.add(langKey)
|
||||||
|
loadedLanguages.add(raw)
|
||||||
triggerLanguageListeners()
|
triggerLanguageListeners()
|
||||||
} catch {
|
} catch {
|
||||||
// Quietly ignore errors
|
// Quietly ignore errors
|
||||||
} finally {
|
} finally {
|
||||||
queuedLanguages.delete(langKey)
|
queuedLanguages.delete(rawToken)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -184,52 +191,6 @@ async function ensureLanguages(content: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeHtmlEntities(content: string): string {
|
|
||||||
if (!content.includes("&")) {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
const entityPattern = /&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]+);/g
|
|
||||||
const namedEntities: Record<string, string> = {
|
|
||||||
amp: "&",
|
|
||||||
lt: "<",
|
|
||||||
gt: ">",
|
|
||||||
quot: '"',
|
|
||||||
apos: "'",
|
|
||||||
nbsp: " ",
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = content
|
|
||||||
let previous = ""
|
|
||||||
|
|
||||||
while (result.includes("&") && result !== previous) {
|
|
||||||
previous = result
|
|
||||||
result = result.replace(entityPattern, (match, entity) => {
|
|
||||||
if (!entity) {
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity[0] === "#") {
|
|
||||||
const isHex = entity[1]?.toLowerCase() === "x"
|
|
||||||
const value = isHex ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10)
|
|
||||||
if (!Number.isNaN(value)) {
|
|
||||||
try {
|
|
||||||
return String.fromCodePoint(value)
|
|
||||||
} catch {
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoded = namedEntities[entity.toLowerCase()]
|
|
||||||
return decoded !== undefined ? decoded : match
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runLanguageLoadQueue() {
|
async function runLanguageLoadQueue() {
|
||||||
if (isQueueRunning || languageLoadQueue.length === 0) {
|
if (isQueueRunning || languageLoadQueue.length === 0) {
|
||||||
return
|
return
|
||||||
@@ -249,7 +210,6 @@ async function runLanguageLoadQueue() {
|
|||||||
|
|
||||||
function setupRenderer(isDark: boolean) {
|
function setupRenderer(isDark: boolean) {
|
||||||
currentTheme = isDark ? "dark" : "light"
|
currentTheme = isDark ? "dark" : "light"
|
||||||
if (!highlighter) return
|
|
||||||
if (rendererSetup) return
|
if (rendererSetup) return
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
@@ -330,8 +290,9 @@ function setupRenderer(isDark: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initMarkdown(isDark: boolean) {
|
export async function initMarkdown(isDark: boolean) {
|
||||||
await getOrCreateHighlighter()
|
|
||||||
setupRenderer(isDark)
|
setupRenderer(isDark)
|
||||||
|
queueHighlighterWarmup()
|
||||||
|
await getOrCreateHighlighter()
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,15 +311,16 @@ export async function renderMarkdown(
|
|||||||
},
|
},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
await initMarkdown(currentTheme === "dark")
|
setupRenderer(currentTheme === "dark")
|
||||||
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const suppressHighlight = options?.suppressHighlight ?? false
|
const suppressHighlight = options?.suppressHighlight ?? false
|
||||||
const decoded = decodeHtmlEntities(content)
|
const decoded = decodeHtmlEntities(content)
|
||||||
|
|
||||||
if (!suppressHighlight) {
|
if (!suppressHighlight) {
|
||||||
// Queue language loading but don't wait for it to complete
|
queueHighlighterWarmup()
|
||||||
await ensureLanguages(decoded)
|
void ensureLanguages(decoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousSuppressed = highlightSuppressed
|
const previousSuppressed = highlightSuppressed
|
||||||
@@ -375,13 +337,3 @@ export async function renderMarkdown(
|
|||||||
export async function getSharedHighlighter(): Promise<Highlighter> {
|
export async function getSharedHighlighter(): Promise<Highlighter> {
|
||||||
return getOrCreateHighlighter()
|
return getOrCreateHighlighter()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeHtml(text: string): string {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
"&": "&",
|
|
||||||
"<": "<",
|
|
||||||
'"': """,
|
|
||||||
"'": "'",
|
|
||||||
}
|
|
||||||
return text.replace(/[&<"']/g, (m) => map[m])
|
|
||||||
}
|
|
||||||
|
|||||||
92
packages/ui/src/lib/text-render-utils.ts
Normal file
92
packages/ui/src/lib/text-render-utils.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
const extensionToLanguage: Record<string, string> = {
|
||||||
|
ts: "typescript",
|
||||||
|
tsx: "typescript",
|
||||||
|
js: "javascript",
|
||||||
|
jsx: "javascript",
|
||||||
|
py: "python",
|
||||||
|
sh: "bash",
|
||||||
|
bash: "bash",
|
||||||
|
json: "json",
|
||||||
|
html: "html",
|
||||||
|
css: "css",
|
||||||
|
md: "markdown",
|
||||||
|
yaml: "yaml",
|
||||||
|
yml: "yaml",
|
||||||
|
sql: "sql",
|
||||||
|
rs: "rust",
|
||||||
|
go: "go",
|
||||||
|
cpp: "cpp",
|
||||||
|
cc: "cpp",
|
||||||
|
cxx: "cpp",
|
||||||
|
hpp: "cpp",
|
||||||
|
h: "cpp",
|
||||||
|
c: "c",
|
||||||
|
java: "java",
|
||||||
|
cs: "csharp",
|
||||||
|
php: "php",
|
||||||
|
rb: "ruby",
|
||||||
|
swift: "swift",
|
||||||
|
kt: "kotlin",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLanguageFromPath(path?: string | null): string | undefined {
|
||||||
|
if (!path) return undefined
|
||||||
|
const ext = path.split(".").pop()?.toLowerCase()
|
||||||
|
return ext ? extensionToLanguage[ext] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeHtmlEntities(content: string): string {
|
||||||
|
if (!content.includes("&")) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityPattern = /&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]+);/g
|
||||||
|
const namedEntities: Record<string, string> = {
|
||||||
|
amp: "&",
|
||||||
|
lt: "<",
|
||||||
|
gt: ">",
|
||||||
|
quot: '"',
|
||||||
|
apos: "'",
|
||||||
|
nbsp: " ",
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = content
|
||||||
|
let previous = ""
|
||||||
|
|
||||||
|
while (result.includes("&") && result !== previous) {
|
||||||
|
previous = result
|
||||||
|
result = result.replace(entityPattern, (match, entity) => {
|
||||||
|
if (!entity) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity[0] === "#") {
|
||||||
|
const isHex = entity[1]?.toLowerCase() === "x"
|
||||||
|
const value = isHex ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10)
|
||||||
|
if (!Number.isNaN(value)) {
|
||||||
|
try {
|
||||||
|
return String.fromCodePoint(value)
|
||||||
|
} catch {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = namedEntities[entity.toLowerCase()]
|
||||||
|
return decoded !== undefined ? decoded : match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeHtml(text: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
'"': """,
|
||||||
|
"'": "'",
|
||||||
|
}
|
||||||
|
return text.replace(/[&<"']/g, (match) => map[match])
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { ThemeProvider } from "./lib/theme"
|
|||||||
import { ConfigProvider } from "./stores/preferences"
|
import { ConfigProvider } from "./stores/preferences"
|
||||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { I18nProvider } from "./lib/i18n"
|
import { I18nProvider, preloadLocaleMessages } from "./lib/i18n"
|
||||||
import { storage } from "./lib/storage"
|
import { storage } from "./lib/storage"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
@@ -31,15 +31,19 @@ async function bootstrap() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const uiConfig = await storage.loadConfigOwner("ui")
|
const uiConfig = await storage.loadConfigOwner("ui")
|
||||||
const theme = (uiConfig as any)?.theme ?? "system"
|
const theme = (uiConfig as any)?.theme
|
||||||
|
const locale = typeof (uiConfig as any)?.settings?.locale === "string" ? (uiConfig as any).settings.locale : undefined
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "light" || theme === "dark") {
|
||||||
document.documentElement.removeAttribute("data-theme")
|
|
||||||
} else {
|
|
||||||
document.documentElement.setAttribute("data-theme", theme)
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute("data-theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await preloadLocaleMessages(locale)
|
||||||
} catch {
|
} catch {
|
||||||
// If config fails to load, fall back to CSS defaults.
|
// If config fails to load, fall back to CSS defaults.
|
||||||
|
await preloadLocaleMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { decodeHtmlEntities } from "../../lib/markdown"
|
import { decodeHtmlEntities } from "../../lib/text-render-utils"
|
||||||
|
|
||||||
function decodeTextSegment(segment: any): any {
|
function decodeTextSegment(segment: any): any {
|
||||||
if (typeof segment === "string") {
|
if (typeof segment === "string") {
|
||||||
|
|||||||
@@ -77,6 +77,29 @@ function shouldSendOsNotification(kind: "needsInput" | "idle"): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChildSession(instanceId: string, sessionId: string): boolean | null {
|
||||||
|
const session = sessions().get(instanceId)?.get(sessionId)
|
||||||
|
if (!session) return null
|
||||||
|
return session.parentId !== null && session.parentId !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSendOsNotificationForSession(
|
||||||
|
kind: "needsInput" | "idle",
|
||||||
|
instanceId: string,
|
||||||
|
sessionId: string | undefined | null,
|
||||||
|
): boolean {
|
||||||
|
if (!shouldSendOsNotification(kind)) return false
|
||||||
|
if (!sessionId) return true
|
||||||
|
|
||||||
|
const child = isChildSession(instanceId, sessionId)
|
||||||
|
|
||||||
|
// Avoid notification spam from spawned child/subagent sessions arriving before hydration.
|
||||||
|
if (child === null) return false
|
||||||
|
if (child) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function getInstanceDisplayName(instanceId: string): string {
|
function getInstanceDisplayName(instanceId: string): string {
|
||||||
const instanceFolder = instances().get(instanceId)?.folder ?? instanceId
|
const instanceFolder = instances().get(instanceId)?.folder ?? instanceId
|
||||||
return instanceFolder.split(/[\\/]/).filter(Boolean).pop() ?? instanceFolder
|
return instanceFolder.split(/[\\/]/).filter(Boolean).pop() ?? instanceFolder
|
||||||
@@ -492,7 +515,7 @@ function handleSessionIdle(instanceId: string, event: EventSessionIdle): void {
|
|||||||
const sessionId = event.properties?.sessionID
|
const sessionId = event.properties?.sessionID
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
|
||||||
if (shouldSendOsNotification("idle")) {
|
if (shouldSendOsNotificationForSession("idle", instanceId, sessionId)) {
|
||||||
const title = getInstanceDisplayName(instanceId)
|
const title = getInstanceDisplayName(instanceId)
|
||||||
const label = getSessionTitle(instanceId, sessionId)
|
const label = getSessionTitle(instanceId, sessionId)
|
||||||
const body = label ? `Session "${label}" is idle` : "Session is idle"
|
const body = label ? `Session "${label}" is idle` : "Session is idle"
|
||||||
@@ -607,9 +630,10 @@ function handlePermissionUpdated(instanceId: string, event: { type: string; prop
|
|||||||
addPermissionToQueue(instanceId, permission)
|
addPermissionToQueue(instanceId, permission)
|
||||||
upsertPermissionV2(instanceId, permission)
|
upsertPermissionV2(instanceId, permission)
|
||||||
|
|
||||||
if (shouldSendOsNotification("needsInput")) {
|
const sessionId = getPermissionSessionId(permission)
|
||||||
|
|
||||||
|
if (shouldSendOsNotificationForSession("needsInput", instanceId, sessionId)) {
|
||||||
const title = getInstanceDisplayName(instanceId)
|
const title = getInstanceDisplayName(instanceId)
|
||||||
const sessionId = getPermissionSessionId(permission)
|
|
||||||
const label = getSessionTitle(instanceId, sessionId)
|
const label = getSessionTitle(instanceId, sessionId)
|
||||||
const body = label ? `Session "${label}" needs permission` : "Session needs permission"
|
const body = label ? `Session "${label}" needs permission` : "Session needs permission"
|
||||||
fireOsNotification({ title, body })
|
fireOsNotification({ title, body })
|
||||||
@@ -634,9 +658,10 @@ function handleQuestionAsked(instanceId: string, event: { type: string; properti
|
|||||||
addQuestionToQueue(instanceId, request)
|
addQuestionToQueue(instanceId, request)
|
||||||
upsertQuestionV2(instanceId, request)
|
upsertQuestionV2(instanceId, request)
|
||||||
|
|
||||||
if (shouldSendOsNotification("needsInput")) {
|
const sessionId = getQuestionSessionId(request)
|
||||||
|
|
||||||
|
if (shouldSendOsNotificationForSession("needsInput", instanceId, sessionId)) {
|
||||||
const title = getInstanceDisplayName(instanceId)
|
const title = getInstanceDisplayName(instanceId)
|
||||||
const sessionId = getQuestionSessionId(request)
|
|
||||||
const label = getSessionTitle(instanceId, sessionId)
|
const label = getSessionTitle(instanceId, sessionId)
|
||||||
const body = label ? `Session "${label}" needs input` : "Session needs input"
|
const body = label ? `Session "${label}" needs input` : "Session needs input"
|
||||||
fireOsNotification({ title, body })
|
fireOsNotification({ title, body })
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
text-align: left;
|
text-align: start;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|||||||
@@ -40,11 +40,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selector-trigger-primary--align-left {
|
.selector-trigger-primary--align-left {
|
||||||
@apply text-left w-full;
|
@apply text-start w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-trigger-secondary {
|
.selector-trigger-secondary {
|
||||||
@apply text-xs text-left truncate;
|
@apply text-xs text-start truncate w-full;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, color-mix(in oklab, var(--surface-secondary) 92%, var(--accent-primary) 8%), var(--surface-secondary));
|
linear-gradient(180deg, color-mix(in oklab, var(--surface-secondary) 92%, var(--accent-primary) 8%), var(--surface-secondary));
|
||||||
border-right: 1px solid var(--border-base);
|
border-inline-end: 1px solid var(--border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-screen-nav-header {
|
.settings-screen-nav-header {
|
||||||
@@ -121,6 +121,9 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transform: translateX(2px);
|
transform: translateX(2px);
|
||||||
}
|
}
|
||||||
|
[dir="rtl"] .settings-nav-button[data-selected="true"] {
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
.settings-nav-button-icon {
|
.settings-nav-button-icon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
@@ -360,7 +363,7 @@
|
|||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
background: var(--surface-base);
|
background: var(--surface-base);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-align: left;
|
text-align: start;
|
||||||
transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -418,7 +421,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-choice-check {
|
.settings-choice-check {
|
||||||
margin-left: auto;
|
margin-inline-start: auto;
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -488,7 +491,7 @@
|
|||||||
.settings-screen-nav {
|
.settings-screen-nav {
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-right: none;
|
border-inline-end: none;
|
||||||
border-bottom: 1px solid var(--border-base);
|
border-bottom: 1px solid var(--border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,21 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Auto-detect text direction per block element for RTL language support (e.g. Hebrew, Arabic) */
|
||||||
|
.markdown-body p,
|
||||||
|
.markdown-body li,
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6,
|
||||||
|
.markdown-body blockquote,
|
||||||
|
.markdown-body td,
|
||||||
|
.markdown-body th {
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-body h1,
|
.markdown-body h1,
|
||||||
.markdown-body h2,
|
.markdown-body h2,
|
||||||
.markdown-body h3,
|
.markdown-body h3,
|
||||||
@@ -121,6 +136,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre:not(.shiki) code,
|
.markdown-body pre:not(.shiki) code,
|
||||||
@@ -129,16 +145,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote {
|
.markdown-body blockquote {
|
||||||
border-left: 3px solid var(--border-base);
|
border-inline-start: 3px solid var(--border-base);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background-color: var(--surface-muted);
|
background-color: var(--surface-muted);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0 8px 8px 0;
|
border-start-start-radius: 0;
|
||||||
|
border-start-end-radius: 8px;
|
||||||
|
border-end-end-radius: 8px;
|
||||||
|
border-end-start-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ul,
|
.markdown-body ul,
|
||||||
.markdown-body ol {
|
.markdown-body ol {
|
||||||
padding-left: 1.5rem;
|
padding-inline-start: 1.5rem;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +185,7 @@
|
|||||||
.markdown-body td {
|
.markdown-body td {
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
text-align: left;
|
text-align: start;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
@@ -221,7 +240,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
|
transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
|
||||||
margin-left: auto;
|
margin-inline-start: auto;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
/* Message error block */
|
/* Message error block */
|
||||||
.message-error-block {
|
.message-error-block {
|
||||||
@apply text-sm p-3 rounded border-l-[3px] my-2;
|
@apply text-sm p-3 rounded border-s-[3px] my-2;
|
||||||
color: var(--status-error);
|
color: var(--status-error);
|
||||||
background-color: var(--message-error-bg);
|
background-color: var(--message-error-bg);
|
||||||
border-color: var(--status-error);
|
border-color: var(--status-error);
|
||||||
|
|||||||
@@ -132,15 +132,22 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-stream-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.message-step-start {
|
.message-step-start {
|
||||||
background-color: var(--message-assistant-bg);
|
background-color: var(--message-assistant-bg);
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-inline-start: 4px solid var(--message-assistant-border);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-step-finish {
|
.message-step-finish {
|
||||||
background-color: var(--message-assistant-bg);
|
background-color: var(--message-assistant-bg);
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-inline-start: 4px solid var(--message-assistant-border);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +172,7 @@
|
|||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
margin-right: 0.35rem;
|
margin-inline-end: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-step-heading {
|
.message-step-heading {
|
||||||
@@ -182,7 +189,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-error-block {
|
.message-error-block {
|
||||||
@apply text-sm p-3 rounded border-l-[3px] my-2;
|
@apply text-sm p-3 rounded border-s-[3px] my-2;
|
||||||
color: var(--status-error);
|
color: var(--status-error);
|
||||||
background-color: var(--message-error-bg);
|
background-color: var(--message-error-bg);
|
||||||
border-color: var(--status-error);
|
border-color: var(--status-error);
|
||||||
@@ -251,6 +258,7 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-error-part {
|
.message-error-part {
|
||||||
@@ -328,12 +336,12 @@
|
|||||||
|
|
||||||
.message-step-start {
|
.message-step-start {
|
||||||
background-color: var(--message-assistant-bg);
|
background-color: var(--message-assistant-bg);
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-inline-start: 4px solid var(--message-assistant-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-step-finish {
|
.message-step-finish {
|
||||||
background-color: var(--message-assistant-bg);
|
background-color: var(--message-assistant-bg);
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-inline-start: 4px solid var(--message-assistant-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-step-heading {
|
.message-step-heading {
|
||||||
@@ -356,7 +364,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-step-time {
|
.message-step-time {
|
||||||
@apply text-[11px] text-[var(--text-muted)] font-normal ml-auto;
|
@apply text-[11px] text-[var(--text-muted)] font-normal ms-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-step-meta-inline {
|
.message-step-meta-inline {
|
||||||
@@ -383,7 +391,7 @@
|
|||||||
.message-reasoning-card {
|
.message-reasoning-card {
|
||||||
--reasoning-border-color: var(--border-strong, var(--border-base));
|
--reasoning-border-color: var(--border-strong, var(--border-base));
|
||||||
background-color: var(--message-assistant-bg);
|
background-color: var(--message-assistant-bg);
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-inline-start: 4px solid var(--message-assistant-border);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -417,7 +425,7 @@
|
|||||||
padding: 0.25rem 0.6rem;
|
padding: 0.25rem 0.6rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-align: left;
|
text-align: start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -539,4 +547,5 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
|
|
||||||
.message-scroll-button-wrapper {
|
.message-scroll-button-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1rem;
|
inset-inline-end: 1rem;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-quote-button + .message-quote-button {
|
.message-quote-button + .message-quote-button {
|
||||||
border-left: 1px solid var(--list-item-highlight-border);
|
border-inline-start: 1px solid var(--list-item-highlight-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-quote-button:hover {
|
.message-quote-button:hover {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
.message-select-checkbox {
|
.message-select-checkbox {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
margin-right: 0.5rem;
|
margin-inline-end: 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
accent-color: var(--status-error);
|
accent-color: var(--status-error);
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-delete-mode-menu {
|
.message-delete-mode-menu {
|
||||||
right: 0;
|
inset-inline-end: 0;
|
||||||
bottom: calc(100% + 6px);
|
bottom: calc(100% + 6px);
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 64px;
|
inset-inline-end: 64px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background-color: var(--border-muted);
|
background-color: var(--border-muted);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-layout--with-timeline::after {
|
.message-layout--with-timeline::after {
|
||||||
right: 40px;
|
inset-inline-end: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,12 +311,12 @@
|
|||||||
|
|
||||||
/* Tool segments that are part of a group get a left accent border. */
|
/* Tool segments that are part of a group get a left accent border. */
|
||||||
.message-timeline-group-child {
|
.message-timeline-group-child {
|
||||||
border-left: 3px solid color-mix(in oklab, var(--accent-primary) 35%, transparent);
|
border-inline-start: 3px solid color-mix(in oklab, var(--accent-primary) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The assistant "parent" at the bottom of a tool group gets the same border. */
|
/* The assistant "parent" at the bottom of a tool group gets the same border. */
|
||||||
.message-timeline-group-parent {
|
.message-timeline-group-parent {
|
||||||
border-left: 3px solid color-mix(in oklab, var(--accent-primary) 35%, transparent);
|
border-inline-start: 3px solid color-mix(in oklab, var(--accent-primary) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra spacing before the first tool in a group to separate from the
|
/* Extra spacing before the first tool in a group to separate from the
|
||||||
@@ -346,7 +346,7 @@
|
|||||||
/* Extend the overlay box into the stream so ribs are not relying on
|
/* Extend the overlay box into the stream so ribs are not relying on
|
||||||
overflow-visible behavior (which is brittle around scroll containers). */
|
overflow-visible behavior (which is brittle around scroll containers). */
|
||||||
--xray-overhang: calc(var(--max-rib-width, 50vw) + 84px);
|
--xray-overhang: calc(var(--max-rib-width, 50vw) + 84px);
|
||||||
left: calc(-1 * var(--xray-overhang));
|
inset-inline-start: calc(-1 * var(--xray-overhang));
|
||||||
width: calc(100% + var(--xray-overhang));
|
width: calc(100% + var(--xray-overhang));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
@@ -374,10 +374,10 @@
|
|||||||
|
|
||||||
.message-timeline-xray-token-label {
|
.message-timeline-xray-token-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 100%;
|
inset-inline-end: 100%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
margin-right: 4px;
|
margin-inline-end: 4px;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -403,16 +403,25 @@
|
|||||||
var(--status-success) calc(100% - var(--segment-weight) * 100%),
|
var(--status-success) calc(100% - var(--segment-weight) * 100%),
|
||||||
var(--status-error) calc(var(--segment-weight) * 100%)
|
var(--status-error) calc(var(--segment-weight) * 100%)
|
||||||
);
|
);
|
||||||
border-radius: 3px 0 0 3px;
|
border-start-start-radius: 3px;
|
||||||
|
border-end-start-radius: 3px;
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
transition: width 0.3s ease, background-color 0.3s ease;
|
transition: width 0.3s ease, background-color 0.3s ease;
|
||||||
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.25);
|
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
[dir="rtl"] .message-timeline-relative-bar {
|
||||||
|
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
.message-timeline-absolute-bar {
|
.message-timeline-absolute-bar {
|
||||||
height: 3px;
|
height: 3px;
|
||||||
width: calc(var(--segment-weight) * var(--max-rib-width, 50vw));
|
width: calc(var(--segment-weight) * var(--max-rib-width, 50vw));
|
||||||
background-color: var(--text-muted);
|
background-color: var(--text-muted);
|
||||||
border-radius: 2px 0 0 2px;
|
border-start-start-radius: 2px;
|
||||||
|
border-end-start-radius: 2px;
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -425,7 +434,7 @@
|
|||||||
.message-timeline-absolute-bar-overflow::before {
|
.message-timeline-absolute-bar-overflow::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -1px;
|
inset-inline-start: -1px;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
bottom: -3px;
|
bottom: -3px;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
|
|||||||
@@ -35,7 +35,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input {
|
.prompt-input {
|
||||||
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
|
@apply w-full pt-2.5 border text-sm resize-none outline-none transition-colors;
|
||||||
|
padding-inline-start: 2.5rem;
|
||||||
|
padding-inline-end: 0.75rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -65,8 +67,8 @@
|
|||||||
.prompt-input-overlay {
|
.prompt-input-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
left: 0.75rem;
|
inset-inline-start: 0.75rem;
|
||||||
right: 0.75rem;
|
inset-inline-end: 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -81,11 +83,13 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation buttons container (expand, prev, next) */
|
/* Navigation buttons container (expand, prev, next).
|
||||||
|
Intentionally at inline-start (left in LTR, right in RTL) so buttons never overlap
|
||||||
|
the scrollbar, which browsers always place at inline-end. */
|
||||||
.prompt-nav-buttons {
|
.prompt-nav-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.25rem;
|
top: 0.25rem;
|
||||||
right: 0.25rem;
|
inset-inline-start: 0.25rem;
|
||||||
bottom: 0.25rem;
|
bottom: 0.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -160,6 +164,11 @@
|
|||||||
@apply opacity-60 cursor-not-allowed;
|
@apply opacity-60 cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* In RTL: override dir="auto" which defaults to LTR on empty textarea */
|
||||||
|
[dir="rtl"] .prompt-input {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-input::placeholder {
|
.prompt-input::placeholder {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
@@ -256,7 +265,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 6px);
|
bottom: calc(100% + 6px);
|
||||||
left: 0;
|
inset-inline-start: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
@@ -335,6 +344,7 @@
|
|||||||
.prompt-input {
|
.prompt-input {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
padding-inline-start: 2.5rem; /* preserve space for nav buttons */
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
.tool-call-message {
|
.tool-call-message {
|
||||||
@apply flex flex-col gap-2 p-3 w-full;
|
@apply flex flex-col gap-2 p-3 w-full;
|
||||||
background-color: var(--message-tool-bg);
|
background-color: var(--message-tool-bg);
|
||||||
border-left: 4px solid var(--message-tool-border);
|
border-inline-start: 4px solid var(--message-tool-border);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-header-toggle {
|
.tool-call-header-toggle {
|
||||||
@apply flex items-center gap-2 p-2 w-full bg-transparent border-none cursor-pointer text-left;
|
@apply flex items-center gap-2 p-2 w-full bg-transparent border-none cursor-pointer text-start;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
.tool-call-header-toggle::before {
|
.tool-call-header-toggle::before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-right: 0.35rem;
|
margin-inline-end: 0.35rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-summary {
|
.tool-call-summary {
|
||||||
@apply flex-1 text-left inline-flex items-center gap-2;
|
@apply flex-1 text-start inline-flex items-center gap-2;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,26 +168,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-summary[data-tool-icon=""]::before {
|
.tool-call-summary[data-tool-icon=""]::before {
|
||||||
margin-right: 0;
|
margin-inline-end: 0;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-summary[data-tool-icon]:not([data-tool-icon=""])::before {
|
.tool-call-summary[data-tool-icon]:not([data-tool-icon=""])::before {
|
||||||
margin-right: 0.35rem;
|
margin-inline-end: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ToolState uses status="completed"; keep "success" as a legacy alias. */
|
/* ToolState uses status="completed"; keep "success" as a legacy alias. */
|
||||||
.tool-call-status-completed,
|
.tool-call-status-completed,
|
||||||
.tool-call-status-success {
|
.tool-call-status-success {
|
||||||
border-left: 3px solid var(--status-success);
|
border-inline-start: 3px solid var(--status-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-status-error {
|
.tool-call-status-error {
|
||||||
border-left: 3px solid var(--status-error);
|
border-inline-start: 3px solid var(--status-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-status-running {
|
.tool-call-status-running {
|
||||||
border-left: 3px solid var(--status-warning);
|
border-inline-start: 3px solid var(--status-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-status-running .tool-call-status {
|
.tool-call-status-running .tool-call-status {
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-status-pending {
|
.tool-call-status-pending {
|
||||||
border-left: 3px solid var(--accent-primary);
|
border-inline-start: 3px solid var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-status-pending .tool-call-summary {
|
.tool-call-status-pending .tool-call-summary {
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--tool-call-border-color);
|
border-bottom: 1px solid var(--tool-call-border-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: start;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -267,7 +267,7 @@
|
|||||||
.tool-call-io-toggle::before {
|
.tool-call-io-toggle::before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-right: 0.35rem;
|
margin-inline-end: 0.35rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-awaiting-permission {
|
.tool-call-awaiting-permission {
|
||||||
border-left-color: var(--status-warning);
|
border-inline-start-color: var(--status-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-permission {
|
.tool-call-permission {
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-permission-shortcuts .kbd {
|
.tool-call-permission-shortcuts .kbd {
|
||||||
margin-right: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-permission-queued-text {
|
.tool-call-permission-queued-text {
|
||||||
@@ -549,6 +549,7 @@
|
|||||||
min-height: auto;
|
min-height: auto;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shiki injects inline background colors; force token surfaces. */
|
/* Shiki injects inline background colors; force token surfaces. */
|
||||||
@@ -610,7 +611,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diagnostics-heading {
|
.tool-call-diagnostics-heading {
|
||||||
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-start;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -634,7 +635,7 @@
|
|||||||
|
|
||||||
|
|
||||||
.tool-call-diagnostics-heading {
|
.tool-call-diagnostics-heading {
|
||||||
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-start;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -693,8 +694,8 @@
|
|||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
max-height: calc(4 * var(--tool-call-line-unit, 1.4em));
|
max-height: calc(4 * var(--tool-call-line-unit, 1.4em));
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-right: 0;
|
padding-inline-end: 0;
|
||||||
margin-right: 0;
|
margin-inline-end: 0;
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
@@ -762,6 +763,7 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em));
|
max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em));
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-section code {
|
.tool-call-section code {
|
||||||
@@ -843,7 +845,7 @@
|
|||||||
|
|
||||||
.tool-call-error-content {
|
.tool-call-error-content {
|
||||||
background-color: var(--message-error-bg);
|
background-color: var(--message-error-bg);
|
||||||
border-left: 3px solid var(--status-error);
|
border-inline-start: 3px solid var(--status-error);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
.tool-call-task-summary .tool-call::before {
|
.tool-call-task-summary .tool-call::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
inset-inline-start: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
padding: 0.35rem 0.5rem 0.35rem 0.75rem;
|
padding: 0.35rem 0.5rem 0.35rem 0.75rem;
|
||||||
border-left: 2px solid var(--tool-call-border-color, var(--border-base));
|
border-inline-start: 2px solid var(--tool-call-border-color, var(--border-base));
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
@@ -134,19 +134,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-task-item[data-task-status="completed"] {
|
.tool-call-task-item[data-task-status="completed"] {
|
||||||
border-left-color: var(--status-success);
|
border-inline-start-color: var(--status-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-task-item[data-task-status="running"] {
|
.tool-call-task-item[data-task-status="running"] {
|
||||||
border-left-color: var(--status-warning);
|
border-inline-start-color: var(--status-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-task-item[data-task-status="pending"] {
|
.tool-call-task-item[data-task-status="pending"] {
|
||||||
border-left-color: var(--accent-primary);
|
border-inline-start-color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-task-item[data-task-status="error"] {
|
.tool-call-task-item[data-task-status="error"] {
|
||||||
border-left-color: var(--status-error);
|
border-inline-start-color: var(--status-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-task-icon {
|
.tool-call-task-icon {
|
||||||
|
|||||||
@@ -1,39 +1,58 @@
|
|||||||
.message-stream {
|
.virtual-follow-list-shell {
|
||||||
@apply flex-1 min-h-0 overflow-y-auto flex flex-col gap-0.5;
|
|
||||||
background-color: var(--surface-base);
|
|
||||||
color: inherit;
|
|
||||||
/* Prevent browser scroll anchoring fighting our virtualization compensation. */
|
|
||||||
overflow-anchor: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-stream-block {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.0625rem;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
contain: layout paint style;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
.virtual-item-wrapper {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-item-placeholder,
|
.message-stream {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
/* Scrolling optimizations */
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
/* Prevents scroll chaining to parent elements */
|
||||||
|
will-change: scroll-position;
|
||||||
|
/* GPU acceleration hint for smoother scrolling */
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
/* Momentum scrolling on iOS */
|
||||||
|
|
||||||
|
/* Prevent browser scroll anchoring fighting our virtualization compensation. */
|
||||||
|
overflow-anchor: none;
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-follow-list-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
/* Ensure it doesn't affect layout at all */
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-follow-list-overlay > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-follow-list-controls-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
|
||||||
|
inset-inline-end: var(--space-md);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
.message-stream-placeholder {
|
.message-stream-placeholder {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-item-content {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtual-item-content-hidden {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-item {
|
.modal-item {
|
||||||
@apply w-full px-4 py-3 flex items-start gap-3 transition-colors cursor-pointer border-none text-left;
|
@apply w-full px-4 py-3 flex items-start gap-3 transition-colors cursor-pointer border-none text-start;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-list-item-content {
|
.panel-list-item-content {
|
||||||
@apply flex-1 text-left px-4 py-3 flex items-center justify-between gap-3 outline-none transition-colors w-full min-w-0;
|
@apply flex-1 text-start px-4 py-3 flex items-center justify-between gap-3 outline-none transition-colors w-full min-w-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-list-item-content:hover {
|
.panel-list-item-content:hover {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
left: 0;
|
inset-inline-start: 0;
|
||||||
right: 0;
|
inset-inline-end: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--border-base);
|
background-color: var(--border-base);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
margin-right: 2px;
|
margin-inline-end: 2px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
.file-split-handle {
|
.file-split-handle {
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-left: 1px solid var(--border-base);
|
border-inline-start: 1px solid var(--border-base);
|
||||||
border-right: 1px solid var(--border-base);
|
border-inline-end: 1px solid var(--border-base);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
border-left: 1px solid var(--border-base);
|
border-inline-start: 1px solid var(--border-base);
|
||||||
/* Monaco uses layered positioned elements; keep overlay well above it. */
|
/* Monaco uses layered positioned elements; keep overlay well above it. */
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
@@ -334,6 +334,7 @@
|
|||||||
.monaco-viewer {
|
.monaco-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-empty {
|
.file-viewer-empty {
|
||||||
@@ -459,7 +460,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
margin-left: 2px;
|
margin-inline-start: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-info-icon {
|
.section-info-icon {
|
||||||
@@ -528,7 +529,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right-panel-empty--left {
|
.right-panel-empty--left {
|
||||||
@apply items-start justify-start text-left w-full;
|
@apply items-start justify-start text-start w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-panel-empty-text {
|
.right-panel-empty-text {
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
inset-inline-start: 0;
|
||||||
width: min(90vw, 360px);
|
width: min(90vw, 360px);
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
border-right: 1px solid var(--border-base);
|
border-inline-end: 1px solid var(--border-base);
|
||||||
box-shadow: var(--folder-card-shadow);
|
box-shadow: var(--folder-card-shadow);
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform 0.25s ease, opacity 0.2s ease;
|
transition: transform 0.25s ease, opacity 0.2s ease;
|
||||||
@@ -41,6 +41,9 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
[dir="rtl"] .session-sidebar-collapsed {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
.session-sidebar-backdrop {
|
.session-sidebar-backdrop {
|
||||||
@apply absolute inset-0;
|
@apply absolute inset-0;
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
.session-sidebar-menu-button--floating {
|
.session-sidebar-menu-button--floating {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
left: 1rem;
|
inset-inline-start: 1rem;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +131,7 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
.mobile-fullscreen-exit-wrapper {
|
.mobile-fullscreen-exit-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: calc(env(safe-area-inset-top, 0px) + 12px);
|
top: calc(env(safe-area-inset-top, 0px) + 12px);
|
||||||
right: calc(env(safe-area-inset-right, 0px) + 12px);
|
inset-inline-end: calc(env(safe-area-inset-right, 0px) + 12px);
|
||||||
z-index: 1250;
|
z-index: 1250;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -143,11 +146,11 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.session-resize-handle--left {
|
.session-resize-handle--left {
|
||||||
right: 0;
|
inset-inline-end: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-resize-handle--right {
|
.session-resize-handle--right {
|
||||||
left: 0;
|
inset-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-resize-handle:hover {
|
.session-resize-handle:hover {
|
||||||
@@ -160,14 +163,20 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.session-resize-handle--left::before {
|
.session-resize-handle--left::before {
|
||||||
right: 0;
|
inset-inline-end: 0;
|
||||||
transform: translateX(50%);
|
transform: translateX(50%);
|
||||||
}
|
}
|
||||||
|
[dir="rtl"] .session-resize-handle--left::before {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
.session-resize-handle--right::before {
|
.session-resize-handle--right::before {
|
||||||
left: 0;
|
inset-inline-start: 0;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
[dir="rtl"] .session-resize-handle--right::before {
|
||||||
|
transform: translateX(50%);
|
||||||
|
}
|
||||||
|
|
||||||
.session-list-header {
|
.session-list-header {
|
||||||
@apply border-b relative;
|
@apply border-b relative;
|
||||||
@@ -190,14 +199,14 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.session-item-base {
|
.session-item-base {
|
||||||
@apply w-full flex flex-col gap-1 px-3 py-2.5 text-left transition-colors outline-none;
|
@apply w-full flex flex-col gap-1 px-3 py-2.5 text-start transition-colors outline-none;
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.session-item-base.session-item-child {
|
.session-item-base.session-item-child {
|
||||||
padding-left: 2.25rem;
|
padding-inline-start: 2.25rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +215,7 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 1.125rem;
|
inset-inline-start: 1.125rem;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background-color: var(--text-secondary);
|
background-color: var(--text-secondary);
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
@@ -221,7 +230,7 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 1.125rem;
|
inset-inline-start: 1.125rem;
|
||||||
width: 0.875rem;
|
width: 0.875rem;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--text-secondary);
|
background-color: var(--text-secondary);
|
||||||
@@ -231,11 +240,11 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.session-item-base.session-item-border-user {
|
.session-item-base.session-item-border-user {
|
||||||
border-left: 4px solid var(--message-user-border);
|
border-inline-start: 4px solid var(--message-user-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-item-base.session-item-border-assistant {
|
.session-item-base.session-item-border-assistant {
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-inline-start: 4px solid var(--message-assistant-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-item-expander {
|
.session-item-expander {
|
||||||
|
|||||||
@@ -77,23 +77,23 @@ export default defineConfig({
|
|||||||
theme_color: "#1a1a1a",
|
theme_color: "#1a1a1a",
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
// Workbox defaults to 2 MiB; our main bundle can slightly exceed that.
|
// Workbox defaults to 2 MiB; our main bundle can slightly exceed that.
|
||||||
// This is a build-time limit for the precache manifest, not a hard runtime cap.
|
// This is a build-time limit for the precache manifest, not a hard runtime cap.
|
||||||
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024,
|
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024,
|
||||||
// Preserve server-side auth redirects (e.g., /login) instead of serving cached index.html.
|
// Preserve server-side auth redirects (e.g., /login) instead of serving cached index.html.
|
||||||
navigateFallback: null,
|
navigateFallback: null,
|
||||||
// Only precache static assets (avoid caching HTML documents / routes).
|
// Only precache static assets (avoid caching HTML documents / routes).
|
||||||
globPatterns: ["**/*.{js,css,png,jpg,jpeg,svg,webp,ico,woff,woff2,ttf,eot,json,webmanifest}"],
|
globPatterns: ["**/*.{js,css,png,jpg,jpeg,svg,webp,ico,woff,woff2,ttf,eot,json,webmanifest}"],
|
||||||
// Monaco assets can be large; cache them at runtime instead.
|
// Monaco assets can be large; cache them at runtime instead.
|
||||||
globIgnores: [
|
globIgnores: [
|
||||||
"**/*.html",
|
"**/*.html",
|
||||||
"**/assets/*worker-*.js",
|
"**/assets/*worker-*.js",
|
||||||
"**/assets/editor.api-*.js",
|
"**/assets/editor.api-*.js",
|
||||||
"**/monaco/vs/**/*",
|
"**/monaco/vs/**/*",
|
||||||
],
|
],
|
||||||
// Only cache static UI assets; never cache API traffic.
|
// Only cache static UI assets; never cache API traffic.
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: ({ url, request }) => {
|
urlPattern: ({ url, request }) => {
|
||||||
if (url.pathname.startsWith("/api/")) return false
|
if (url.pathname.startsWith("/api/")) return false
|
||||||
if (request.destination === "document") return false
|
if (request.destination === "document") return false
|
||||||
@@ -134,6 +134,34 @@ export default defineConfig({
|
|||||||
main: resolve(__dirname, "./src/renderer/index.html"),
|
main: resolve(__dirname, "./src/renderer/index.html"),
|
||||||
loading: resolve(__dirname, "./src/renderer/loading.html"),
|
loading: resolve(__dirname, "./src/renderer/loading.html"),
|
||||||
},
|
},
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
const normalizedId = id.replace(/\\/g, "/")
|
||||||
|
|
||||||
|
if (normalizedId.includes("/node_modules/@git-diff-view/")) {
|
||||||
|
return "git-diff-vendor"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedId.includes("/node_modules/highlight.js/") || normalizedId.includes("/node_modules/lowlight/")) {
|
||||||
|
return "highlight-vendor"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedId.includes("/node_modules/fast-diff/")) {
|
||||||
|
return "fast-diff-vendor"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedId.includes("/node_modules/monaco-editor/")) {
|
||||||
|
return "monaco-vendor"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedId.includes("/src/components/file-viewer/") ||
|
||||||
|
normalizedId.includes("/src/lib/monaco/")
|
||||||
|
) {
|
||||||
|
return "monaco-viewer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user