ui: Linting & Formatting scripts (#26819)
This commit is contained in:
+50
-14
@@ -1,14 +1,15 @@
|
|||||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||||
import storybook from 'eslint-plugin-storybook';
|
import svelteConfig from './svelte.config.js';
|
||||||
|
|
||||||
import prettier from 'eslint-config-prettier';
|
|
||||||
import { includeIgnoreFile } from '@eslint/compat';
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import perfectionist from 'eslint-plugin-perfectionist';
|
||||||
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
|
import storybook from 'eslint-plugin-storybook';
|
||||||
import svelte from 'eslint-plugin-svelte';
|
import svelte from 'eslint-plugin-svelte';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import ts from 'typescript-eslint';
|
import ts from 'typescript-eslint';
|
||||||
import svelteConfig from './svelte.config.js';
|
|
||||||
|
|
||||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||||
|
|
||||||
@@ -21,32 +22,67 @@ export default ts.config(
|
|||||||
...svelte.configs.prettier,
|
...svelte.configs.prettier,
|
||||||
{
|
{
|
||||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||||
|
plugins: { perfectionist, 'simple-import-sort': simpleImportSort },
|
||||||
rules: {
|
rules: {
|
||||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
|
||||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
|
||||||
'no-undef': 'off',
|
|
||||||
'svelte/no-at-html-tags': 'off',
|
|
||||||
// This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
|
|
||||||
'svelte/no-navigation-without-resolve': 'off',
|
|
||||||
|
|
||||||
// Snippet bodies often ignore one or more of the parent's params
|
// Snippet bodies often ignore one or more of the parent's params
|
||||||
// (e.g. `{#snippet children(_meta, ctx)}` when only ctx is read).
|
// (e.g. `{#snippet children(_meta, ctx)}` when only ctx is read).
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
||||||
],
|
],
|
||||||
|
|
||||||
// Enforce empty line at end of file
|
// Enforce empty line at end of file
|
||||||
'eol-last': 'error'
|
'eol-last': 'error',
|
||||||
|
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||||
|
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||||
|
'no-undef': 'off',
|
||||||
|
|
||||||
|
'padding-line-between-statements': [
|
||||||
|
'error',
|
||||||
|
// Blank line between function/class declarations.
|
||||||
|
{ blankLine: 'always', next: ['function', 'class'], prev: ['function', 'class'] },
|
||||||
|
// Blank line around if blocks (if/else and else if stay one statement).
|
||||||
|
{ blankLine: 'always', next: '*', prev: 'if' },
|
||||||
|
{ blankLine: 'always', next: 'if', prev: '*' },
|
||||||
|
// Blank line after the last declaration in a group. Because the 'never'
|
||||||
|
// rules below are scoped per declaration kind, a const group and a let
|
||||||
|
// group get separated by a blank line, while same-kind declarations stay
|
||||||
|
// together.
|
||||||
|
{ blankLine: 'always', next: '*', prev: ['const', 'let', 'var'] },
|
||||||
|
// No blank line between consecutive declarations of the same kind (kept
|
||||||
|
// last so each takes precedence over the always rule above for matching
|
||||||
|
// declaration pairs).
|
||||||
|
{ blankLine: 'never', next: 'const', prev: 'const' },
|
||||||
|
{ blankLine: 'never', next: 'let', prev: 'let' },
|
||||||
|
{ blankLine: 'never', next: 'var', prev: 'var' },
|
||||||
|
// Blank line before a statement that follows another statement in the block
|
||||||
|
// (works for return/throw/break/continue). A blank line for a terminal
|
||||||
|
// statement that opens a block body can't be enforced here: Prettier removes
|
||||||
|
// the leading blank line of a block, so the two formatters would fight.
|
||||||
|
{ blankLine: 'always', next: ['return', 'throw', 'break', 'continue'], prev: '*' }
|
||||||
|
],
|
||||||
|
|
||||||
|
'perfectionist/sort-objects': ['error', { type: 'natural' }],
|
||||||
|
|
||||||
|
// Alphabetical order for variable declarations and object keys
|
||||||
|
'perfectionist/sort-variable-declarations': ['error', { type: 'natural' }],
|
||||||
|
|
||||||
|
// Sort imports alphabetically by module path, and sort named members within
|
||||||
|
// each statement. A single catch-all group keeps the list flat (no blank-line
|
||||||
|
// grouping); Prettier normalizes comma spacing afterwards.
|
||||||
|
'simple-import-sort/imports': ['error', { groups: [['.*']] }],
|
||||||
|
'svelte/no-at-html-tags': 'off',
|
||||||
|
|
||||||
|
// This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
|
||||||
|
'svelte/no-navigation-without-resolve': 'off'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
|
||||||
extraFileExtensions: ['.svelte'],
|
extraFileExtensions: ['.svelte'],
|
||||||
parser: ts.parser,
|
parser: ts.parser,
|
||||||
|
projectService: true,
|
||||||
svelteConfig
|
svelteConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+232
@@ -39,6 +39,8 @@
|
|||||||
"dompurify": "3.4.13",
|
"dompurify": "3.4.13",
|
||||||
"eslint": "9.39.4",
|
"eslint": "9.39.4",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-perfectionist": "^5.10.1",
|
||||||
|
"eslint-plugin-simple-import-sort": "^14.0.0",
|
||||||
"eslint-plugin-storybook": "10.5.6",
|
"eslint-plugin-storybook": "10.5.6",
|
||||||
"eslint-plugin-svelte": "3.19.0",
|
"eslint-plugin-svelte": "3.19.0",
|
||||||
"fflate": "0.8.3",
|
"fflate": "0.8.3",
|
||||||
@@ -9281,6 +9283,226 @@
|
|||||||
"eslint": ">=7.0.0"
|
"eslint": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist": {
|
||||||
|
"version": "5.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.10.1.tgz",
|
||||||
|
"integrity": "sha512-Kprsp9Us0GqAesYaAIzUViw57xYp5WBqzXrcE0Mtww++E5fexWXYBipMuuD7yvyH4vvpBH0+oJ+OMAmZ0oYXkw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/utils": "^8.65.0",
|
||||||
|
"natural-orderby": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || >=22.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.45.0 || ^9.0.0 || ^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/project-service": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/tsconfig-utils": "^8.66.0",
|
||||||
|
"@typescript-eslint/types": "^8.66.0",
|
||||||
|
"debug": "^4.4.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/scope-manager": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.66.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.66.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/types": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/typescript-estree": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/project-service": "8.66.0",
|
||||||
|
"@typescript-eslint/tsconfig-utils": "8.66.0",
|
||||||
|
"@typescript-eslint/types": "8.66.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.66.0",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"semver": "^7.7.3",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"ts-api-utils": "^2.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/utils": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
|
"@typescript-eslint/scope-manager": "8.66.0",
|
||||||
|
"@typescript-eslint/types": "8.66.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "8.66.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/visitor-keys": {
|
||||||
|
"version": "8.66.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz",
|
||||||
|
"integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.66.0",
|
||||||
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-perfectionist/node_modules/minimatch": {
|
||||||
|
"version": "10.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||||
|
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-simple-import-sort": {
|
||||||
|
"version": "14.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-14.0.0.tgz",
|
||||||
|
"integrity": "sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">=5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-storybook": {
|
"node_modules/eslint-plugin-storybook": {
|
||||||
"version": "10.5.6",
|
"version": "10.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.6.tgz",
|
||||||
@@ -13196,6 +13418,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/natural-orderby": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"reset": "rm -rf .svelte-kit node_modules",
|
"reset": "rm -rf .svelte-kit node_modules",
|
||||||
"format": "prettier --write .",
|
"format": "eslint --fix . && prettier --write .",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"test": "npm run test:ui -- --run && npm run test:client -- --run && npm run test:unit -- --run && npm run test:e2e",
|
"test": "npm run test:ui -- --run && npm run test:client -- --run && npm run test:unit -- --run && npm run test:e2e",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@storybook/addon-a11y": "10.5.6",
|
"@storybook/addon-a11y": "10.5.6",
|
||||||
"@storybook/addon-docs": "10.5.6",
|
"@storybook/addon-docs": "10.5.6",
|
||||||
|
"@storybook/addon-mcp": "0.7.0",
|
||||||
"@storybook/addon-svelte-csf": "5.1.2",
|
"@storybook/addon-svelte-csf": "5.1.2",
|
||||||
"@storybook/addon-vitest": "10.5.6",
|
"@storybook/addon-vitest": "10.5.6",
|
||||||
"@storybook/sveltekit": "10.5.6",
|
"@storybook/sveltekit": "10.5.6",
|
||||||
@@ -57,6 +58,8 @@
|
|||||||
"dompurify": "3.4.13",
|
"dompurify": "3.4.13",
|
||||||
"eslint": "9.39.4",
|
"eslint": "9.39.4",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-perfectionist": "^5.10.1",
|
||||||
|
"eslint-plugin-simple-import-sort": "^14.0.0",
|
||||||
"eslint-plugin-storybook": "10.5.6",
|
"eslint-plugin-storybook": "10.5.6",
|
||||||
"eslint-plugin-svelte": "3.19.0",
|
"eslint-plugin-svelte": "3.19.0",
|
||||||
"fflate": "0.8.3",
|
"fflate": "0.8.3",
|
||||||
@@ -99,8 +102,7 @@
|
|||||||
"vite-plugin-devtools-json": "0.2.1",
|
"vite-plugin-devtools-json": "0.2.1",
|
||||||
"vitest": "4.1.10",
|
"vitest": "4.1.10",
|
||||||
"vitest-browser-svelte": "2.1.1",
|
"vitest-browser-svelte": "2.1.1",
|
||||||
"workbox-window": "7.4.1",
|
"workbox-window": "7.4.1"
|
||||||
"@storybook/addon-mcp": "0.7.0"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"cookie": "1.1.1",
|
"cookie": "1.1.1",
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: 'tests/e2e',
|
|
||||||
testMatch: ['**/*.e2e.ts'],
|
|
||||||
timeout: 30000,
|
|
||||||
expect: {
|
expect: {
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
},
|
},
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
fullyParallel: true,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: 'line',
|
|
||||||
use: {
|
|
||||||
baseURL: 'http://localhost:8181',
|
|
||||||
trace: 'on-first-retry'
|
|
||||||
},
|
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
use: { ...devices['Desktop Chrome'] }
|
use: { ...devices['Desktop Chrome'] }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
reporter: 'line',
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
testDir: 'tests/e2e',
|
||||||
|
testMatch: ['**/*.e2e.ts'],
|
||||||
|
timeout: 30000,
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:8181',
|
||||||
|
trace: 'on-first-retry'
|
||||||
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run build && npx http-server ./dist -p 8181',
|
command: 'npm run build && npx http-server ./dist -p 8181',
|
||||||
port: 8181,
|
port: 8181,
|
||||||
timeout: 120000,
|
reuseExistingServer: !process.env.CI,
|
||||||
reuseExistingServer: !process.env.CI
|
timeout: 120000
|
||||||
}
|
},
|
||||||
|
workers: process.env.CI ? 1 : undefined
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineConfig } from '@vite-pwa/assets-generator/config';
|
|
||||||
import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
|
|
||||||
import { writeThemeFavicons } from './scripts/favicon-colorize';
|
import { writeThemeFavicons } from './scripts/favicon-colorize';
|
||||||
|
import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
|
||||||
|
import { defineConfig } from '@vite-pwa/assets-generator/config';
|
||||||
|
|
||||||
writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
|
writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
|
||||||
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
|
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
|
||||||
@@ -10,18 +10,18 @@ export default defineConfig({
|
|||||||
headLinkOptions: {
|
headLinkOptions: {
|
||||||
preset: '2023'
|
preset: '2023'
|
||||||
},
|
},
|
||||||
|
images: ['static/favicon-dark.svg'],
|
||||||
preset: {
|
preset: {
|
||||||
transparent: {
|
apple: {
|
||||||
sizes: [],
|
sizes: []
|
||||||
favicons: [[48, 'favicon-dark.ico']],
|
|
||||||
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
|
|
||||||
},
|
},
|
||||||
maskable: {
|
maskable: {
|
||||||
sizes: []
|
sizes: []
|
||||||
},
|
},
|
||||||
apple: {
|
transparent: {
|
||||||
|
favicons: [[48, 'favicon-dark.ico']],
|
||||||
|
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING,
|
||||||
sizes: []
|
sizes: []
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
images: ['static/favicon-dark.svg']
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import { writeThemeFavicons } from './scripts/favicon-colorize';
|
||||||
|
import {
|
||||||
|
FAVICON_COLORS,
|
||||||
|
PWA_ASSET_GENERATOR,
|
||||||
|
PWA_GENERATOR_DEVICES,
|
||||||
|
THEME_COLORS
|
||||||
|
} from './src/lib/constants/pwa';
|
||||||
|
import { SplashOrientation } from './src/lib/enums/splash.enums';
|
||||||
import {
|
import {
|
||||||
combinePresetAndAppleSplashScreens,
|
combinePresetAndAppleSplashScreens,
|
||||||
defineConfig,
|
defineConfig,
|
||||||
@@ -5,14 +13,6 @@ import {
|
|||||||
} from '@vite-pwa/assets-generator/config';
|
} from '@vite-pwa/assets-generator/config';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import {
|
|
||||||
THEME_COLORS,
|
|
||||||
PWA_GENERATOR_DEVICES,
|
|
||||||
PWA_ASSET_GENERATOR,
|
|
||||||
FAVICON_COLORS
|
|
||||||
} from './src/lib/constants/pwa';
|
|
||||||
import { SplashOrientation } from './src/lib/enums/splash.enums';
|
|
||||||
import { writeThemeFavicons } from './scripts/favicon-colorize';
|
|
||||||
|
|
||||||
writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
|
writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
|
||||||
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
|
padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
|
||||||
@@ -22,6 +22,7 @@ export default defineConfig({
|
|||||||
headLinkOptions: {
|
headLinkOptions: {
|
||||||
preset: PWA_ASSET_GENERATOR.LINK_PRESET
|
preset: PWA_ASSET_GENERATOR.LINK_PRESET
|
||||||
},
|
},
|
||||||
|
images: ['static/favicon.svg'],
|
||||||
preset: combinePresetAndAppleSplashScreens(
|
preset: combinePresetAndAppleSplashScreens(
|
||||||
{
|
{
|
||||||
...minimal2023Preset,
|
...minimal2023Preset,
|
||||||
@@ -32,37 +33,37 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
padding: PWA_ASSET_GENERATOR.SPLASH_PADDING,
|
|
||||||
resizeOptions: {
|
|
||||||
background: THEME_COLORS.BACKGROUND_LIGHT,
|
|
||||||
fit: PWA_ASSET_GENERATOR.FIT_MODE
|
|
||||||
},
|
|
||||||
darkResizeOptions: {
|
|
||||||
background: THEME_COLORS.BACKGROUND_DARK,
|
|
||||||
fit: PWA_ASSET_GENERATOR.FIT_MODE
|
|
||||||
},
|
|
||||||
darkImageResolver: async (imageName: string) => {
|
darkImageResolver: async (imageName: string) => {
|
||||||
if (imageName.endsWith('favicon.svg')) {
|
if (imageName.endsWith('favicon.svg')) {
|
||||||
return readFileSync(resolve('static/favicon-dark.svg'));
|
return readFileSync(resolve('static/favicon-dark.svg'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
darkResizeOptions: {
|
||||||
|
background: THEME_COLORS.BACKGROUND_DARK,
|
||||||
|
fit: PWA_ASSET_GENERATOR.FIT_MODE
|
||||||
|
},
|
||||||
linkMediaOptions: {
|
linkMediaOptions: {
|
||||||
log: true,
|
|
||||||
addMediaScreen: PWA_ASSET_GENERATOR.ADD_MEDIA_SCREEN,
|
addMediaScreen: PWA_ASSET_GENERATOR.ADD_MEDIA_SCREEN,
|
||||||
basePath: PWA_ASSET_GENERATOR.BASE_PATH,
|
basePath: PWA_ASSET_GENERATOR.BASE_PATH,
|
||||||
|
log: true,
|
||||||
xhtml: PWA_ASSET_GENERATOR.XHTML
|
xhtml: PWA_ASSET_GENERATOR.XHTML
|
||||||
},
|
},
|
||||||
png: {
|
|
||||||
compressionLevel: PWA_ASSET_GENERATOR.PNG_COMPRESSION_LEVEL,
|
|
||||||
quality: PWA_ASSET_GENERATOR.PNG_QUALITY
|
|
||||||
},
|
|
||||||
name: (landscape, size, dark) => {
|
name: (landscape, size, dark) => {
|
||||||
const orientation = landscape ? SplashOrientation.LANDSCAPE : SplashOrientation.PORTRAIT;
|
const orientation = landscape ? SplashOrientation.LANDSCAPE : SplashOrientation.PORTRAIT;
|
||||||
const darkPrefix = dark ? PWA_ASSET_GENERATOR.DARK_PREFIX : '';
|
const darkPrefix = dark ? PWA_ASSET_GENERATOR.DARK_PREFIX : '';
|
||||||
|
|
||||||
return `apple-splash-${orientation}-${darkPrefix}${size.width}x${size.height}.png`;
|
return `apple-splash-${orientation}-${darkPrefix}${size.width}x${size.height}.png`;
|
||||||
|
},
|
||||||
|
padding: PWA_ASSET_GENERATOR.SPLASH_PADDING,
|
||||||
|
png: {
|
||||||
|
compressionLevel: PWA_ASSET_GENERATOR.PNG_COMPRESSION_LEVEL,
|
||||||
|
quality: PWA_ASSET_GENERATOR.PNG_QUALITY
|
||||||
|
},
|
||||||
|
resizeOptions: {
|
||||||
|
background: THEME_COLORS.BACKGROUND_LIGHT,
|
||||||
|
fit: PWA_ASSET_GENERATOR.FIT_MODE
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PWA_GENERATOR_DEVICES
|
PWA_GENERATOR_DEVICES
|
||||||
),
|
)
|
||||||
images: ['static/favicon.svg']
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
const PROJECT_ROOT = resolve(HERE, '..');
|
const PROJECT_ROOT = resolve(HERE, '..');
|
||||||
|
|
||||||
const DEFAULT_LOGO = resolve(PROJECT_ROOT, 'src/lib/assets/logo.svg');
|
const DEFAULT_LOGO = resolve(PROJECT_ROOT, 'src/lib/assets/logo.svg');
|
||||||
const DEFAULT_OUT_DIR = resolve(PROJECT_ROOT, 'static');
|
const DEFAULT_OUT_DIR = resolve(PROJECT_ROOT, 'static');
|
||||||
const DEFAULT_OUT_LIGHT = resolve(DEFAULT_OUT_DIR, 'favicon.svg');
|
const DEFAULT_OUT_LIGHT = resolve(DEFAULT_OUT_DIR, 'favicon.svg');
|
||||||
const DEFAULT_OUT_DARK = resolve(DEFAULT_OUT_DIR, 'favicon-dark.svg');
|
const DEFAULT_OUT_DARK = resolve(DEFAULT_OUT_DIR, 'favicon-dark.svg');
|
||||||
|
|
||||||
const CURRENT_COLOR = 'currentColor';
|
const CURRENT_COLOR = 'currentColor';
|
||||||
|
|
||||||
export interface ColorizedFavicon {
|
export interface ColorizedFavicon {
|
||||||
@@ -39,8 +37,8 @@ export function colorizeFaviconSvg(
|
|||||||
darkColor: string
|
darkColor: string
|
||||||
): ColorizedFavicon {
|
): ColorizedFavicon {
|
||||||
return {
|
return {
|
||||||
light: svg.replaceAll(CURRENT_COLOR, lightColor),
|
dark: svg.replaceAll(CURRENT_COLOR, darkColor),
|
||||||
dark: svg.replaceAll(CURRENT_COLOR, darkColor)
|
light: svg.replaceAll(CURRENT_COLOR, lightColor)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,33 +52,40 @@ export function padFaviconSvg(svg: string, padding: number): string {
|
|||||||
if (!(padding > 0) || padding >= 1) return svg;
|
if (!(padding > 0) || padding >= 1) return svg;
|
||||||
|
|
||||||
const viewBoxMatch = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
const viewBoxMatch = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
||||||
|
|
||||||
if (!viewBoxMatch) return svg;
|
if (!viewBoxMatch) return svg;
|
||||||
|
|
||||||
const parts = viewBoxMatch[1]
|
const parts = viewBoxMatch[1]
|
||||||
.trim()
|
.trim()
|
||||||
.split(/[\s,]+/)
|
.split(/[\s,]+/)
|
||||||
.map(Number);
|
.map(Number);
|
||||||
|
|
||||||
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return svg;
|
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return svg;
|
||||||
|
|
||||||
const [, , width, height] = parts;
|
const [, , width, height] = parts;
|
||||||
|
|
||||||
if (width <= 0 || height <= 0) return svg;
|
if (width <= 0 || height <= 0) return svg;
|
||||||
|
|
||||||
const scale = 1 - padding;
|
const scale = 1 - padding;
|
||||||
const translateX = (padding * width) / 2;
|
const translateX = (padding * width) / 2;
|
||||||
const translateY = (padding * height) / 2;
|
const translateY = (padding * height) / 2;
|
||||||
|
|
||||||
const openTagStart = svg.search(/<svg\b/i);
|
const openTagStart = svg.search(/<svg\b/i);
|
||||||
|
|
||||||
if (openTagStart === -1) return svg;
|
if (openTagStart === -1) return svg;
|
||||||
|
|
||||||
const openTagEnd = svg.indexOf('>', openTagStart);
|
const openTagEnd = svg.indexOf('>', openTagStart);
|
||||||
|
|
||||||
if (openTagEnd === -1) return svg;
|
if (openTagEnd === -1) return svg;
|
||||||
|
|
||||||
const closeStart = svg.lastIndexOf('</svg');
|
const closeStart = svg.lastIndexOf('</svg');
|
||||||
|
|
||||||
if (closeStart === -1 || closeStart <= openTagEnd) return svg;
|
if (closeStart === -1 || closeStart <= openTagEnd) return svg;
|
||||||
|
|
||||||
const openTag = svg.slice(0, openTagEnd + 1);
|
const openTag = svg.slice(0, openTagEnd + 1);
|
||||||
const inner = svg.slice(openTagEnd + 1, closeStart);
|
const inner = svg.slice(openTagEnd + 1, closeStart);
|
||||||
const closeTag = svg.slice(closeStart);
|
const closeTag = svg.slice(closeStart);
|
||||||
|
|
||||||
const group = `<g transform="translate(${translateX} ${translateY}) scale(${scale})">`;
|
const group = `<g transform="translate(${translateX} ${translateY}) scale(${scale})">`;
|
||||||
|
|
||||||
return `${openTag}${group}${inner}</g>${closeTag}`;
|
return `${openTag}${group}${inner}</g>${closeTag}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +98,15 @@ export function writeThemeFavicons(
|
|||||||
lightColor: string,
|
lightColor: string,
|
||||||
darkColor: string,
|
darkColor: string,
|
||||||
{
|
{
|
||||||
sourcePath = DEFAULT_LOGO,
|
|
||||||
lightOutPath = DEFAULT_OUT_LIGHT,
|
|
||||||
darkOutPath = DEFAULT_OUT_DARK,
|
darkOutPath = DEFAULT_OUT_DARK,
|
||||||
padding = 0
|
lightOutPath = DEFAULT_OUT_LIGHT,
|
||||||
|
padding = 0,
|
||||||
|
sourcePath = DEFAULT_LOGO
|
||||||
}: WriteThemeFaviconsOptions = {}
|
}: WriteThemeFaviconsOptions = {}
|
||||||
): void {
|
): void {
|
||||||
const source = readFileSync(sourcePath, 'utf-8');
|
const source = readFileSync(sourcePath, 'utf-8');
|
||||||
const { light, dark } = colorizeFaviconSvg(source, lightColor, darkColor);
|
const { dark, light } = colorizeFaviconSvg(source, lightColor, darkColor);
|
||||||
|
|
||||||
mkdirSync(dirname(lightOutPath), { recursive: true });
|
mkdirSync(dirname(lightOutPath), { recursive: true });
|
||||||
writeFileSync(lightOutPath, padFaviconSvg(light, padding));
|
writeFileSync(lightOutPath, padFaviconSvg(light, padding));
|
||||||
writeFileSync(darkOutPath, padFaviconSvg(dark, padding));
|
writeFileSync(darkOutPath, padFaviconSvg(dark, padding));
|
||||||
|
|||||||
@@ -13,31 +13,28 @@
|
|||||||
* maskable-icon and apple-touch-icon are left untouched.
|
* maskable-icon and apple-touch-icon are left untouched.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import sharp from 'sharp';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const STATIC_DIR = path.resolve(__dirname, '..', 'static');
|
const STATIC_DIR = path.resolve(__dirname, '..', 'static');
|
||||||
|
|
||||||
const paddingPct = process.argv.reduce((acc, arg, i, args) => {
|
const paddingPct = process.argv.reduce((acc, arg, i, args) => {
|
||||||
if (arg === '--padding-pct' && args[i + 1]) return parseFloat(args[i + 1]);
|
if (arg === '--padding-pct' && args[i + 1]) return parseFloat(args[i + 1]);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Scale down the source image before cropping to circle
|
// Scale down the source image before cropping to circle
|
||||||
const scalePct = process.argv.reduce((acc, arg, i, args) => {
|
const scalePct = process.argv.reduce((acc, arg, i, args) => {
|
||||||
if (arg === '--scale-pct' && args[i + 1]) return parseFloat(args[i + 1]);
|
if (arg === '--scale-pct' && args[i + 1]) return parseFloat(args[i + 1]);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, 85); // default 85% - icon fills 85% of the circular area
|
}, 85); // default 85% - icon fills 85% of the circular area
|
||||||
|
|
||||||
// Source for circular icons: the maskable icon (white bg, full logo)
|
// Source for circular icons: the maskable icon (white bg, full logo)
|
||||||
const sourceIcon = 'maskable-icon-512x512.png';
|
const sourceIcon = 'maskable-icon-512x512.png';
|
||||||
const targetIcons = ['pwa-64x64.png', 'pwa-192x192.png', 'pwa-512x512.png'];
|
const targetIcons = ['pwa-64x64.png', 'pwa-192x192.png', 'pwa-512x512.png'];
|
||||||
|
|
||||||
// maskable-icon and apple-touch-icon stay square
|
// maskable-icon and apple-touch-icon stay square
|
||||||
const untouchedIcons = ['maskable-icon-512x512.png', 'apple-touch-icon-180x180.png'];
|
const untouchedIcons = ['maskable-icon-512x512.png', 'apple-touch-icon-180x180.png'];
|
||||||
|
|
||||||
@@ -47,10 +44,13 @@ async function makeCircle(targetFilename) {
|
|||||||
|
|
||||||
if (!fs.existsSync(sourcePath)) {
|
if (!fs.existsSync(sourcePath)) {
|
||||||
console.log(`⏭️ ${sourceIcon} not found, skipping`);
|
console.log(`⏭️ ${sourceIcon} not found, skipping`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(targetPath)) {
|
if (!fs.existsSync(targetPath)) {
|
||||||
console.log(`⏭️ ${targetFilename} not found, skipping`);
|
console.log(`⏭️ ${targetFilename} not found, skipping`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,16 +58,18 @@ async function makeCircle(targetFilename) {
|
|||||||
const size = Math.max(metadata.width, metadata.height);
|
const size = Math.max(metadata.width, metadata.height);
|
||||||
const radius = Math.floor((size * (1 - paddingPct / 100)) / 2);
|
const radius = Math.floor((size * (1 - paddingPct / 100)) / 2);
|
||||||
const center = Math.floor(size / 2);
|
const center = Math.floor(size / 2);
|
||||||
|
|
||||||
// Build circular mask as RGBA buffer: white opaque circle on transparent bg
|
// Build circular mask as RGBA buffer: white opaque circle on transparent bg
|
||||||
const maskBuf = Buffer.alloc(size * size * 4, 0);
|
const maskBuf = Buffer.alloc(size * size * 4, 0);
|
||||||
|
|
||||||
for (let y = 0; y < size; y++) {
|
for (let y = 0; y < size; y++) {
|
||||||
for (let x = 0; x < size; x++) {
|
for (let x = 0; x < size; x++) {
|
||||||
const dx = x - center;
|
const dx = x - center;
|
||||||
const dy = y - center;
|
const dy = y - center;
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
if (dist < radius) {
|
if (dist < radius) {
|
||||||
const i = (y * size + x) * 4;
|
const i = (y * size + x) * 4;
|
||||||
|
|
||||||
maskBuf[i] = 255;
|
maskBuf[i] = 255;
|
||||||
maskBuf[i + 1] = 255;
|
maskBuf[i + 1] = 255;
|
||||||
maskBuf[i + 2] = 255;
|
maskBuf[i + 2] = 255;
|
||||||
@@ -77,8 +79,9 @@ async function makeCircle(targetFilename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tmpMask = path.join(STATIC_DIR, '.mask-tmp.png');
|
const tmpMask = path.join(STATIC_DIR, '.mask-tmp.png');
|
||||||
|
|
||||||
await sharp(maskBuf, {
|
await sharp(maskBuf, {
|
||||||
raw: { width: size, height: size, channels: 4 }
|
raw: { channels: 4, height: size, width: size }
|
||||||
})
|
})
|
||||||
.png()
|
.png()
|
||||||
.toFile(tmpMask);
|
.toFile(tmpMask);
|
||||||
@@ -87,28 +90,26 @@ async function makeCircle(targetFilename) {
|
|||||||
const circleDiameter = Math.floor(size * (1 - paddingPct / 100));
|
const circleDiameter = Math.floor(size * (1 - paddingPct / 100));
|
||||||
const scaledSize = Math.floor((circleDiameter * scalePct) / 100);
|
const scaledSize = Math.floor((circleDiameter * scalePct) / 100);
|
||||||
const offset = Math.floor((size - scaledSize) / 2);
|
const offset = Math.floor((size - scaledSize) / 2);
|
||||||
|
|
||||||
const scaledBuf = await sharp(sourcePath)
|
const scaledBuf = await sharp(sourcePath)
|
||||||
.resize(scaledSize, scaledSize, {
|
.resize(scaledSize, scaledSize, {
|
||||||
fit: 'cover',
|
background: { alpha: 1, b: 255, g: 255, r: 255 },
|
||||||
background: { r: 255, g: 255, b: 255, alpha: 1 }
|
fit: 'cover'
|
||||||
})
|
})
|
||||||
.ensureAlpha()
|
.ensureAlpha()
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
// Step 2: Composite scaled image onto white background, then apply circular mask
|
// Step 2: Composite scaled image onto white background, then apply circular mask
|
||||||
const output = await sharp({
|
const output = await sharp({
|
||||||
create: {
|
create: {
|
||||||
width: size,
|
background: { alpha: 1, b: 255, g: 255, r: 255 },
|
||||||
height: size,
|
|
||||||
channels: 4,
|
channels: 4,
|
||||||
background: { r: 255, g: 255, b: 255, alpha: 1 }
|
height: size,
|
||||||
|
width: size
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.composite([
|
.composite([
|
||||||
{ input: scaledBuf, top: offset, left: offset },
|
{ input: scaledBuf, left: offset, top: offset },
|
||||||
{ input: tmpMask, top: 0, left: 0, blend: 'dest-in' }
|
{ blend: 'dest-in', input: tmpMask, left: 0, top: 0 }
|
||||||
])
|
])
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
@@ -130,6 +131,7 @@ async function main() {
|
|||||||
console.log('\nUnchanged:');
|
console.log('\nUnchanged:');
|
||||||
for (const icon of untouchedIcons) {
|
for (const icon of untouchedIcons) {
|
||||||
const fp = path.join(STATIC_DIR, icon);
|
const fp = path.join(STATIC_DIR, icon);
|
||||||
|
|
||||||
console.log(` ${icon} (${fs.existsSync(fp) ? fs.statSync(fp).size + ' bytes' : 'missing'})`);
|
console.log(` ${icon} (${fs.existsSync(fp) ? fs.statSync(fp).size + ' bytes' : 'missing'})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { writeFileSync, existsSync } from 'node:fs';
|
import { BUILD_CONFIG } from '../src/lib/constants/pwa';
|
||||||
|
import { existsSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { BUILD_CONFIG } from '../src/lib/constants/pwa';
|
|
||||||
|
|
||||||
let processed = false;
|
let processed = false;
|
||||||
|
|
||||||
@@ -15,27 +15,29 @@ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
|
|||||||
*/
|
*/
|
||||||
export function buildInfoPlugin(): Plugin {
|
export function buildInfoPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
name: 'llamacpp:build-info',
|
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
closeBundle() {
|
closeBundle() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
if (processed) return;
|
if (processed) return;
|
||||||
|
|
||||||
processed = true;
|
processed = true;
|
||||||
|
|
||||||
const buildNumber = process.env.LLAMA_BUILD_NUMBER || 'b0000';
|
const buildNumber = process.env.LLAMA_BUILD_NUMBER || 'b0000';
|
||||||
|
|
||||||
const outDir = resolve(OUTPUT_DIR);
|
const outDir = resolve(OUTPUT_DIR);
|
||||||
const indexPath = resolve(outDir, 'index.html');
|
const indexPath = resolve(outDir, 'index.html');
|
||||||
|
|
||||||
if (!existsSync(indexPath)) return;
|
if (!existsSync(indexPath)) return;
|
||||||
|
|
||||||
const buildJsonPath = resolve(outDir, 'build.json');
|
const buildJsonPath = resolve(outDir, 'build.json');
|
||||||
|
|
||||||
writeFileSync(buildJsonPath, JSON.stringify({ version: buildNumber }), 'utf-8');
|
writeFileSync(buildJsonPath, JSON.stringify({ version: buildNumber }), 'utf-8');
|
||||||
console.log(`Created build.json (version: ${buildNumber})`);
|
console.log(`Created build.json (version: ${buildNumber})`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to write build.json:', error);
|
console.error('Failed to write build.json:', error);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
},
|
||||||
|
name: 'llamacpp:build-info'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { fileURLToPath } from 'url';
|
|||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const VENDORS_DIR = resolve(__dirname, '../src/lib/vendors');
|
const VENDORS_DIR = resolve(__dirname, '../src/lib/vendors');
|
||||||
const VIRTUAL_ID = 'virtual:nerdamer';
|
const VIRTUAL_ID = 'virtual:nerdamer';
|
||||||
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
||||||
@@ -21,29 +20,32 @@ export function nerdamerPlugin(): Plugin {
|
|||||||
let bundled: string | null = null;
|
let bundled: string | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'llamacpp:nerdamer',
|
|
||||||
resolveId(id) {
|
|
||||||
return id === VIRTUAL_ID ? RESOLVED_ID : undefined;
|
|
||||||
},
|
|
||||||
async load(id) {
|
async load(id) {
|
||||||
if (id !== RESOLVED_ID) return undefined;
|
if (id !== RESOLVED_ID) return undefined;
|
||||||
|
|
||||||
if (bundled === null) {
|
if (bundled === null) {
|
||||||
const result = await build({
|
const result = await build({
|
||||||
entryPoints: [resolve(VENDORS_DIR, 'nerdamer-prime/all.js')],
|
|
||||||
bundle: true,
|
|
||||||
minify: true,
|
|
||||||
format: 'iife',
|
|
||||||
globalName: 'nerdamer',
|
|
||||||
alias: {
|
alias: {
|
||||||
'big-integer': resolve(VENDORS_DIR, 'big-integer/BigInteger.js'),
|
'big-integer': resolve(VENDORS_DIR, 'big-integer/BigInteger.js'),
|
||||||
'decimal.js': resolve(VENDORS_DIR, 'decimal.js/decimal.js')
|
'decimal.js': resolve(VENDORS_DIR, 'decimal.js/decimal.js')
|
||||||
},
|
},
|
||||||
write: false,
|
bundle: true,
|
||||||
logLevel: 'silent'
|
entryPoints: [resolve(VENDORS_DIR, 'nerdamer-prime/all.js')],
|
||||||
|
format: 'iife',
|
||||||
|
globalName: 'nerdamer',
|
||||||
|
logLevel: 'silent',
|
||||||
|
minify: true,
|
||||||
|
write: false
|
||||||
});
|
});
|
||||||
|
|
||||||
bundled = result.outputFiles[0].text;
|
bundled = result.outputFiles[0].text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `export default ${JSON.stringify(bundled)};`;
|
return `export default ${JSON.stringify(bundled)};`;
|
||||||
|
},
|
||||||
|
name: 'llamacpp:nerdamer',
|
||||||
|
resolveId(id) {
|
||||||
|
return id === VIRTUAL_ID ? RESOLVED_ID : undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
import { BUILD_CONFIG } from '../src/lib/constants/pwa';
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { BUILD_CONFIG } from '../src/lib/constants/pwa';
|
|
||||||
|
|
||||||
let processed = false;
|
let processed = false;
|
||||||
|
|
||||||
@@ -11,11 +11,15 @@ function rewrite(path: string, pairs: [string, string][]): void {
|
|||||||
if (!existsSync(path)) {
|
if (!existsSync(path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = readFileSync(path, 'utf-8');
|
const text = readFileSync(path, 'utf-8');
|
||||||
|
|
||||||
let out = text;
|
let out = text;
|
||||||
|
|
||||||
for (const [from, to] of pairs) {
|
for (const [from, to] of pairs) {
|
||||||
out = out.split(from).join(to);
|
out = out.split(from).join(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (out !== text) {
|
if (out !== text) {
|
||||||
writeFileSync(path, out, 'utf-8');
|
writeFileSync(path, out, 'utf-8');
|
||||||
}
|
}
|
||||||
@@ -32,12 +36,12 @@ function rewrite(path: string, pairs: [string, string][]): void {
|
|||||||
*/
|
*/
|
||||||
export function relativizeBasePlugin(): Plugin {
|
export function relativizeBasePlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
name: 'llamacpp:relativize-base',
|
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
closeBundle() {
|
closeBundle() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
if (processed) return;
|
if (processed) return;
|
||||||
|
|
||||||
processed = true;
|
processed = true;
|
||||||
|
|
||||||
const outDir = resolve(OUTPUT_DIR);
|
const outDir = resolve(OUTPUT_DIR);
|
||||||
@@ -56,6 +60,7 @@ export function relativizeBasePlugin(): Plugin {
|
|||||||
console.error('Failed to relativize base refs:', error);
|
console.error('Failed to relativize base refs:', error);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
},
|
||||||
|
name: 'llamacpp:relativize-base'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
import { NEWLINE, TAB } from '../src/lib/constants/code';
|
||||||
|
import { APPLE_DEVICES, BUILD_CONFIG, REGEX_PATTERNS, SPLASH_LINK } from '../src/lib/constants/pwa';
|
||||||
|
import { SplashOrientation } from '../src/lib/enums/splash.enums';
|
||||||
|
import type { SplashDimensions } from '../src/lib/types';
|
||||||
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { TAB, NEWLINE } from '../src/lib/constants/code';
|
|
||||||
import { APPLE_DEVICES, BUILD_CONFIG, REGEX_PATTERNS, SPLASH_LINK } from '../src/lib/constants/pwa';
|
|
||||||
import type { SplashDimensions } from '../src/lib/types';
|
|
||||||
import { SplashOrientation } from '../src/lib/enums/splash.enums';
|
|
||||||
|
|
||||||
let processed = false;
|
let processed = false;
|
||||||
|
|
||||||
@@ -16,23 +16,26 @@ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
|
|||||||
*/
|
*/
|
||||||
export function generateSplashScreenLinks(outDir: string): string[] {
|
export function generateSplashScreenLinks(outDir: string): string[] {
|
||||||
const files = readdirSync(outDir).filter((f) => f.match(REGEX_PATTERNS.SPLASH_FILE));
|
const files = readdirSync(outDir).filter((f) => f.match(REGEX_PATTERNS.SPLASH_FILE));
|
||||||
|
|
||||||
if (files.length === 0) return [];
|
if (files.length === 0) return [];
|
||||||
|
|
||||||
const dimMap = new Map<string, SplashDimensions>();
|
const dimMap = new Map<string, SplashDimensions>();
|
||||||
|
|
||||||
for (const [dims, spec] of Object.entries(APPLE_DEVICES)) {
|
for (const [dims, spec] of Object.entries(APPLE_DEVICES)) {
|
||||||
const [w, h] = dims.split('x').map(Number);
|
const [w, h] = dims.split('x').map(Number);
|
||||||
|
|
||||||
// logical-point dimensions
|
// logical-point dimensions
|
||||||
dimMap.set(`${w}x${h}`, { deviceW: spec.width, deviceH: spec.height, dpr: spec.dpr });
|
dimMap.set(`${w}x${h}`, { deviceH: spec.height, deviceW: spec.width, dpr: spec.dpr });
|
||||||
dimMap.set(`${h}x${w}`, { deviceW: spec.width, deviceH: spec.height, dpr: spec.dpr });
|
dimMap.set(`${h}x${w}`, { deviceH: spec.height, deviceW: spec.width, dpr: spec.dpr });
|
||||||
// pixel dimensions (used by actual generated splash files)
|
// pixel dimensions (used by actual generated splash files)
|
||||||
dimMap.set(`${w * spec.dpr}x${h * spec.dpr}`, {
|
dimMap.set(`${w * spec.dpr}x${h * spec.dpr}`, {
|
||||||
deviceW: spec.width,
|
|
||||||
deviceH: spec.height,
|
deviceH: spec.height,
|
||||||
|
deviceW: spec.width,
|
||||||
dpr: spec.dpr
|
dpr: spec.dpr
|
||||||
});
|
});
|
||||||
dimMap.set(`${h * spec.dpr}x${w * spec.dpr}`, {
|
dimMap.set(`${h * spec.dpr}x${w * spec.dpr}`, {
|
||||||
deviceW: spec.width,
|
|
||||||
deviceH: spec.height,
|
deviceH: spec.height,
|
||||||
|
deviceW: spec.width,
|
||||||
dpr: spec.dpr
|
dpr: spec.dpr
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -42,20 +45,23 @@ export function generateSplashScreenLinks(outDir: string): string[] {
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const match = file.match(REGEX_PATTERNS.SPLASH_FILE);
|
const match = file.match(REGEX_PATTERNS.SPLASH_FILE);
|
||||||
|
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
|
|
||||||
const orientation = match[1] as SplashOrientation;
|
const orientation = match[1] as SplashOrientation;
|
||||||
const isDark = !!match[2];
|
const isDark = !!match[2];
|
||||||
const pixelW = parseInt(match[3]);
|
const pixelW = parseInt(match[3]);
|
||||||
const pixelH = parseInt(match[4]);
|
const pixelH = parseInt(match[4]);
|
||||||
|
|
||||||
const key = `${pixelW}x${pixelH}`;
|
const key = `${pixelW}x${pixelH}`;
|
||||||
const spec = dimMap.get(key);
|
const spec = dimMap.get(key);
|
||||||
|
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
console.warn(`Unknown splash screen dimensions: ${key} (${file})`);
|
console.warn(`Unknown splash screen dimensions: ${key} (${file})`);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { deviceW, deviceH, dpr } = spec;
|
const { deviceH, deviceW, dpr } = spec;
|
||||||
const media = `screen and (device-width: ${deviceW}px) and (device-height: ${deviceH}px) and (-webkit-device-pixel-ratio: ${dpr}) and (orientation: ${orientation})`;
|
const media = `screen and (device-width: ${deviceW}px) and (device-height: ${deviceH}px) and (-webkit-device-pixel-ratio: ${dpr}) and (orientation: ${orientation})`;
|
||||||
const href = `./${file}`;
|
const href = `./${file}`;
|
||||||
|
|
||||||
@@ -73,16 +79,17 @@ export function generateSplashScreenLinks(outDir: string): string[] {
|
|||||||
|
|
||||||
export function splashScreenPlugin(): Plugin {
|
export function splashScreenPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
name: 'llamacpp:splash-screen',
|
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
closeBundle() {
|
closeBundle() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
if (processed) return;
|
if (processed) return;
|
||||||
|
|
||||||
processed = true;
|
processed = true;
|
||||||
|
|
||||||
const outDir = resolve(OUTPUT_DIR);
|
const outDir = resolve(OUTPUT_DIR);
|
||||||
const indexPath = resolve(outDir, 'index.html');
|
const indexPath = resolve(outDir, 'index.html');
|
||||||
|
|
||||||
if (!existsSync(indexPath)) return;
|
if (!existsSync(indexPath)) return;
|
||||||
|
|
||||||
let content = readFileSync(indexPath, 'utf-8');
|
let content = readFileSync(indexPath, 'utf-8');
|
||||||
@@ -91,9 +98,11 @@ export function splashScreenPlugin(): Plugin {
|
|||||||
// The @vite-pwa/assets-generator generates apple-splash-*.png files;
|
// The @vite-pwa/assets-generator generates apple-splash-*.png files;
|
||||||
// this scans them and creates the <link> tags SvelteKit needs.
|
// this scans them and creates the <link> tags SvelteKit needs.
|
||||||
const splashLinks = generateSplashScreenLinks(outDir);
|
const splashLinks = generateSplashScreenLinks(outDir);
|
||||||
|
|
||||||
if (splashLinks.length > 0) {
|
if (splashLinks.length > 0) {
|
||||||
console.log(`Generated ${splashLinks.length} apple-splash link tags`);
|
console.log(`Generated ${splashLinks.length} apple-splash link tags`);
|
||||||
const splashHtml = splashLinks.map((l) => TAB + TAB + l).join(NEWLINE);
|
const splashHtml = splashLinks.map((l) => TAB + TAB + l).join(NEWLINE);
|
||||||
|
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
REGEX_PATTERNS.HEAD_CLOSE,
|
REGEX_PATTERNS.HEAD_CLOSE,
|
||||||
splashHtml + NEWLINE + TAB + TAB + '</head>'
|
splashHtml + NEWLINE + TAB + TAB + '</head>'
|
||||||
@@ -110,6 +119,7 @@ export function splashScreenPlugin(): Plugin {
|
|||||||
console.error('Failed to process build output:', error);
|
console.error('Failed to process build output:', error);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
},
|
||||||
|
name: 'llamacpp:splash-screen'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+14
-17
@@ -3,9 +3,8 @@
|
|||||||
|
|
||||||
import 'vite-plugin-pwa/pwa-assets';
|
import 'vite-plugin-pwa/pwa-assets';
|
||||||
import 'vite-plugin-pwa/svelte';
|
import 'vite-plugin-pwa/svelte';
|
||||||
|
import { ModelModality, ServerModelStatus, ServerRole } from '$lib/enums';
|
||||||
// Import chat types from dedicated module
|
// Import chat types from dedicated module
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
// API types
|
// API types
|
||||||
ApiChatCompletionRequest,
|
ApiChatCompletionRequest,
|
||||||
@@ -13,59 +12,57 @@ import type {
|
|||||||
ApiChatCompletionStreamChunk,
|
ApiChatCompletionStreamChunk,
|
||||||
ApiChatCompletionToolCall,
|
ApiChatCompletionToolCall,
|
||||||
ApiChatCompletionToolCallDelta,
|
ApiChatCompletionToolCallDelta,
|
||||||
ApiChatMessageData,
|
|
||||||
ApiChatMessageContentPart,
|
ApiChatMessageContentPart,
|
||||||
|
ApiChatMessageData,
|
||||||
ApiContextSizeError,
|
ApiContextSizeError,
|
||||||
ApiErrorResponse,
|
ApiErrorResponse,
|
||||||
ApiLlamaCppServerProps,
|
ApiLlamaCppServerProps,
|
||||||
ApiModelDataEntry,
|
ApiModelDataEntry,
|
||||||
|
ApiModelListResponse,
|
||||||
ApiModelLoadStage,
|
ApiModelLoadStage,
|
||||||
ApiModelsSseProgress,
|
|
||||||
ApiModelsSseData,
|
ApiModelsSseData,
|
||||||
ApiModelsSseEvent,
|
ApiModelsSseEvent,
|
||||||
ApiModelListResponse,
|
ApiModelsSseProgress,
|
||||||
ApiProcessingState,
|
ApiProcessingState,
|
||||||
ApiRouterModelMeta,
|
ApiRouterModelMeta,
|
||||||
|
ApiRouterModelsListResponse,
|
||||||
ApiRouterModelsLoadRequest,
|
ApiRouterModelsLoadRequest,
|
||||||
ApiRouterModelsLoadResponse,
|
ApiRouterModelsLoadResponse,
|
||||||
ApiRouterModelsStatusRequest,
|
ApiRouterModelsStatusRequest,
|
||||||
ApiRouterModelsStatusResponse,
|
ApiRouterModelsStatusResponse,
|
||||||
ApiRouterModelsListResponse,
|
|
||||||
ApiRouterModelsUnloadRequest,
|
ApiRouterModelsUnloadRequest,
|
||||||
ApiRouterModelsUnloadResponse,
|
ApiRouterModelsUnloadResponse,
|
||||||
// Chat types
|
// Chat types
|
||||||
ChatAttachmentDisplayItem,
|
ChatAttachmentDisplayItem,
|
||||||
|
ChatMessagePromptProgress,
|
||||||
|
ChatMessageSiblingInfo,
|
||||||
|
ChatMessageTimings,
|
||||||
ChatMessageType,
|
ChatMessageType,
|
||||||
ChatRole,
|
ChatRole,
|
||||||
ChatUploadedFile,
|
ChatUploadedFile,
|
||||||
ChatMessageSiblingInfo,
|
|
||||||
ChatMessagePromptProgress,
|
|
||||||
ChatMessageTimings,
|
|
||||||
// Database types
|
// Database types
|
||||||
DatabaseConversation,
|
DatabaseConversation,
|
||||||
DatabaseMessage,
|
DatabaseMessage,
|
||||||
DatabaseMessageExtra,
|
DatabaseMessageExtra,
|
||||||
DatabaseMessageExtraAudioFile,
|
DatabaseMessageExtraAudioFile,
|
||||||
DatabaseMessageExtraVideoFile,
|
|
||||||
DatabaseMessageExtraImageFile,
|
DatabaseMessageExtraImageFile,
|
||||||
DatabaseMessageExtraTextFile,
|
|
||||||
DatabaseMessageExtraPdfFile,
|
|
||||||
DatabaseMessageExtraLegacyContext,
|
DatabaseMessageExtraLegacyContext,
|
||||||
|
DatabaseMessageExtraPdfFile,
|
||||||
|
DatabaseMessageExtraTextFile,
|
||||||
|
DatabaseMessageExtraVideoFile,
|
||||||
ExportedConversation,
|
ExportedConversation,
|
||||||
ExportedConversations,
|
ExportedConversations,
|
||||||
|
ModelLoadProgress,
|
||||||
// Model types
|
// Model types
|
||||||
ModelModalities,
|
ModelModalities,
|
||||||
ModelOption,
|
ModelOption,
|
||||||
ModelLoadProgress,
|
|
||||||
// Settings types
|
// Settings types
|
||||||
SettingsChatServiceOptions,
|
SettingsChatServiceOptions,
|
||||||
|
SettingsConfigType,
|
||||||
SettingsConfigValue,
|
SettingsConfigValue,
|
||||||
SettingsFieldConfig,
|
SettingsFieldConfig
|
||||||
SettingsConfigType
|
|
||||||
} from '$lib/types';
|
} from '$lib/types';
|
||||||
|
|
||||||
import { ServerRole, ServerModelStatus, ModelModality } from '$lib/enums';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// namespace App {
|
// namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, type ButtonVariant, type ButtonSize } from '$lib/components/ui/button';
|
import { Button, type ButtonSize, type ButtonVariant } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import type { Component } from 'svelte';
|
|
||||||
import { TooltipSide } from '$lib/enums';
|
import { TooltipSide } from '$lib/enums';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
@@ -20,18 +20,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
icon,
|
ariaLabel,
|
||||||
tooltip,
|
|
||||||
variant = 'ghost',
|
|
||||||
href = '',
|
|
||||||
size = 'sm',
|
|
||||||
class: className = '',
|
class: className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
href = '',
|
||||||
|
icon,
|
||||||
iconSize = 'h-3 w-3',
|
iconSize = 'h-3 w-3',
|
||||||
tooltipSide = TooltipSide.TOP,
|
|
||||||
stopPropagationOnClick = false,
|
|
||||||
onclick,
|
onclick,
|
||||||
ariaLabel
|
size = 'sm',
|
||||||
|
stopPropagationOnClick = false,
|
||||||
|
tooltip,
|
||||||
|
tooltipSide = TooltipSide.TOP,
|
||||||
|
variant = 'ghost'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let innerWidth = $state(0);
|
let innerWidth = $state(0);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
|
||||||
import { Copy } from '@lucide/svelte';
|
|
||||||
import { copyToClipboard } from '$lib/utils';
|
|
||||||
import ActionIcon from './ActionIcon.svelte';
|
import ActionIcon from './ActionIcon.svelte';
|
||||||
|
import { Copy } from '@lucide/svelte';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
export let ariaLabel: string = 'Copy to clipboard';
|
export let ariaLabel: string = 'Copy to clipboard';
|
||||||
export let canCopy: boolean = true;
|
export let canCopy: boolean = true;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class?: string;
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { modalities, class: className = '' }: Props = $props();
|
let { class: className = '', modalities }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each modalities as modality (modality)}
|
{#each modalities as modality (modality)}
|
||||||
|
|||||||
+7
-7
@@ -28,18 +28,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
activeModelId,
|
||||||
style = '',
|
|
||||||
attachments = [],
|
attachments = [],
|
||||||
readonly = false,
|
class: className = '',
|
||||||
onFileRemove,
|
|
||||||
uploadedFiles = $bindable([]),
|
|
||||||
// Default to small size for form previews
|
// Default to small size for form previews
|
||||||
imageClass = '',
|
imageClass = '',
|
||||||
imageHeight = 'h-24',
|
imageHeight = 'h-24',
|
||||||
imageWidth = 'w-auto',
|
imageWidth = 'w-auto',
|
||||||
limitToSingleRow = false,
|
limitToSingleRow = false,
|
||||||
activeModelId
|
onFileRemove,
|
||||||
|
readonly = false,
|
||||||
|
style = '',
|
||||||
|
uploadedFiles = $bindable([])
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let carouselRef: HorizontalScrollCarousel | undefined = $state();
|
let carouselRef: HorizontalScrollCarousel | undefined = $state();
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
let previewFocusIndex = $state(0);
|
let previewFocusIndex = $state(0);
|
||||||
let viewAllDialogOpen = $state(false);
|
let viewAllDialogOpen = $state(false);
|
||||||
|
|
||||||
let displayItems = $derived(getAttachmentDisplayItems({ uploadedFiles, attachments }));
|
let displayItems = $derived(getAttachmentDisplayItems({ attachments, uploadedFiles }));
|
||||||
|
|
||||||
function openPreview(item: ChatAttachmentDisplayItem, event?: MouseEvent) {
|
function openPreview(item: ChatAttachmentDisplayItem, event?: MouseEvent) {
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
|
|||||||
+9
-9
@@ -2,8 +2,8 @@
|
|||||||
import {
|
import {
|
||||||
ChatAttachmentsListItemMcpPrompt,
|
ChatAttachmentsListItemMcpPrompt,
|
||||||
ChatAttachmentsListItemMcpResource,
|
ChatAttachmentsListItemMcpResource,
|
||||||
ChatAttachmentsListItemThumbnailImage,
|
ChatAttachmentsListItemThumbnailFile,
|
||||||
ChatAttachmentsListItemThumbnailFile
|
ChatAttachmentsListItemThumbnailImage
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { AttachmentType } from '$lib/enums';
|
import { AttachmentType } from '$lib/enums';
|
||||||
import type {
|
import type {
|
||||||
@@ -49,10 +49,10 @@
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
resource: {
|
resource: {
|
||||||
uri: extra.uri,
|
|
||||||
name: extra.name,
|
name: extra.name,
|
||||||
|
serverName: extra.serverName,
|
||||||
title: extra.name,
|
title: extra.name,
|
||||||
serverName: extra.serverName
|
uri: extra.uri
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -64,12 +64,12 @@
|
|||||||
? (item.attachment as DatabaseMessageExtraMcpPrompt)
|
? (item.attachment as DatabaseMessageExtraMcpPrompt)
|
||||||
: item.uploadedFile?.mcpPrompt
|
: item.uploadedFile?.mcpPrompt
|
||||||
? {
|
? {
|
||||||
type: AttachmentType.MCP_PROMPT as const,
|
arguments: item.uploadedFile.mcpPrompt.arguments,
|
||||||
name: item.name,
|
|
||||||
serverName: item.uploadedFile.mcpPrompt.serverName,
|
|
||||||
promptName: item.uploadedFile.mcpPrompt.promptName,
|
|
||||||
content: item.textContent ?? '',
|
content: item.textContent ?? '',
|
||||||
arguments: item.uploadedFile.mcpPrompt.arguments
|
name: item.name,
|
||||||
|
promptName: item.uploadedFile.mcpPrompt.promptName,
|
||||||
|
serverName: item.uploadedFile.mcpPrompt.serverName,
|
||||||
|
type: AttachmentType.MCP_PROMPT as const
|
||||||
}
|
}
|
||||||
: null}
|
: null}
|
||||||
{#if mcpPrompt}
|
{#if mcpPrompt}
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChatMessageMcpPromptContent, ActionIcon } from '$lib/components/app';
|
|
||||||
import { X } from '@lucide/svelte';
|
import { X } from '@lucide/svelte';
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
import { ActionIcon, ChatMessageMcpPromptContent } from '$lib/components/app';
|
||||||
import { McpPromptVariant } from '$lib/enums';
|
import { McpPromptVariant } from '$lib/enums';
|
||||||
|
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
+6
-5
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Loader2, AlertCircle } from '@lucide/svelte';
|
import { AlertCircle, Loader2 } from '@lucide/svelte';
|
||||||
|
import { X } from '@lucide/svelte';
|
||||||
|
import { ActionIcon } from '$lib/components/app';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
import type { MCPResourceAttachment } from '$lib/types';
|
import type { MCPResourceAttachment } from '$lib/types';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import { getResourceDisplayName, getResourceIcon } from '$lib/utils';
|
||||||
import { ActionIcon } from '$lib/components/app';
|
|
||||||
import { X } from '@lucide/svelte';
|
|
||||||
import { getResourceIcon, getResourceDisplayName } from '$lib/utils';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: MCPResourceAttachment;
|
attachment: MCPResourceAttachment;
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
function getStatusClass(attachment: MCPResourceAttachment): string {
|
function getStatusClass(attachment: MCPResourceAttachment): string {
|
||||||
if (attachment.error) return 'border-red-500/50 bg-red-500/10';
|
if (attachment.error) return 'border-red-500/50 bg-red-500/10';
|
||||||
|
|
||||||
if (attachment.loading) return 'border-border/50 bg-muted/30';
|
if (attachment.loading) return 'border-border/50 bg-muted/30';
|
||||||
|
|
||||||
return 'border-border/50 bg-muted/30';
|
return 'border-border/50 bg-muted/30';
|
||||||
|
|||||||
+7
-7
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Music, Video, X } from '@lucide/svelte';
|
||||||
|
import { ActionIcon } from '$lib/components/app';
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
import { X, Music, Video } from '@lucide/svelte';
|
import { AttachmentType } from '$lib/enums';
|
||||||
import {
|
import {
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
getFileTypeLabel,
|
getFileTypeLabel,
|
||||||
getPreviewText,
|
getPreviewText,
|
||||||
isPdfFile,
|
|
||||||
isAudioFile,
|
isAudioFile,
|
||||||
isVideoFile,
|
isPdfFile,
|
||||||
isTextFile
|
isTextFile,
|
||||||
|
isVideoFile
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { ActionIcon } from '$lib/components/app';
|
|
||||||
import { AttachmentType } from '$lib/enums';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment?: DatabaseMessageExtra;
|
attachment?: DatabaseMessageExtra;
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
attachment,
|
attachment,
|
||||||
class: className = '',
|
class: className = '',
|
||||||
id,
|
id,
|
||||||
|
name,
|
||||||
onclick,
|
onclick,
|
||||||
onRemove,
|
onRemove,
|
||||||
name,
|
|
||||||
readonly = false,
|
readonly = false,
|
||||||
size,
|
size,
|
||||||
textContent,
|
textContent,
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionIcon } from '$lib/components/app';
|
|
||||||
import { X } from '@lucide/svelte';
|
import { X } from '@lucide/svelte';
|
||||||
|
import { ActionIcon } from '$lib/components/app';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
height = 'h-16',
|
height = 'h-16',
|
||||||
id,
|
id,
|
||||||
imageClass = '',
|
imageClass = '',
|
||||||
|
name,
|
||||||
onclick,
|
onclick,
|
||||||
onRemove,
|
onRemove,
|
||||||
name,
|
|
||||||
preview,
|
preview,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
width = 'w-auto'
|
width = 'w-auto'
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
getAttachmentDisplayItems,
|
getAttachmentDisplayItems,
|
||||||
getLanguageFromFilename,
|
getLanguageFromFilename,
|
||||||
isAudioFile,
|
isAudioFile,
|
||||||
isVideoFile,
|
|
||||||
isImageFile,
|
isImageFile,
|
||||||
isMcpPrompt,
|
isMcpPrompt,
|
||||||
isMcpResource,
|
isMcpResource,
|
||||||
isPdfFile,
|
isPdfFile,
|
||||||
isTextFile
|
isTextFile,
|
||||||
|
isVideoFile
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
|
||||||
interface PreviewItem {
|
interface PreviewItem {
|
||||||
@@ -42,21 +42,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
uploadedFiles = [],
|
|
||||||
attachments = [],
|
|
||||||
activeModelId,
|
activeModelId,
|
||||||
|
attachments = [],
|
||||||
class: className = '',
|
class: className = '',
|
||||||
previewFocusIndex = 0
|
previewFocusIndex = 0,
|
||||||
|
uploadedFiles = []
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let allItems = $derived(
|
let allItems = $derived(
|
||||||
getAttachmentDisplayItems({ uploadedFiles, attachments })
|
getAttachmentDisplayItems({ attachments, uploadedFiles })
|
||||||
.filter((item) => !isMcpPrompt(item) && !isMcpResource(item))
|
.filter((item) => !isMcpPrompt(item) && !isMcpResource(item))
|
||||||
.map(
|
.map(
|
||||||
(item): PreviewItem => ({
|
(item): PreviewItem => ({
|
||||||
...item,
|
...item,
|
||||||
isImage: isImageFile(item.attachment, item.uploadedFile),
|
|
||||||
isAudio: isAudioFile(item.attachment, item.uploadedFile),
|
isAudio: isAudioFile(item.attachment, item.uploadedFile),
|
||||||
|
isImage: isImageFile(item.attachment, item.uploadedFile),
|
||||||
isVideo: isVideoFile(item.attachment, item.uploadedFile)
|
isVideo: isVideoFile(item.attachment, item.uploadedFile)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -88,10 +88,11 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const index = currentIndex;
|
const index = currentIndex;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
|
const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
|
||||||
|
|
||||||
thumbnail?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
thumbnail?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ChatAttachmentDisplayItem } from '$lib/types';
|
|
||||||
import { Image, Music, Video, FileText, FileIcon } from '@lucide/svelte';
|
|
||||||
import ChatAttachmentsPreviewCurrentItemPdf from './ChatAttachmentsPreviewCurrentItemPdf.svelte';
|
|
||||||
import ChatAttachmentsPreviewCurrentItemImage from './ChatAttachmentsPreviewCurrentItemImage.svelte';
|
|
||||||
import ChatAttachmentsPreviewCurrentItemAudio from './ChatAttachmentsPreviewCurrentItemAudio.svelte';
|
import ChatAttachmentsPreviewCurrentItemAudio from './ChatAttachmentsPreviewCurrentItemAudio.svelte';
|
||||||
import ChatAttachmentsPreviewCurrentItemVideo from './ChatAttachmentsPreviewCurrentItemVideo.svelte';
|
import ChatAttachmentsPreviewCurrentItemImage from './ChatAttachmentsPreviewCurrentItemImage.svelte';
|
||||||
|
import ChatAttachmentsPreviewCurrentItemPdf from './ChatAttachmentsPreviewCurrentItemPdf.svelte';
|
||||||
import ChatAttachmentsPreviewCurrentItemText from './ChatAttachmentsPreviewCurrentItemText.svelte';
|
import ChatAttachmentsPreviewCurrentItemText from './ChatAttachmentsPreviewCurrentItemText.svelte';
|
||||||
import ChatAttachmentsPreviewCurrentItemUnavailable from './ChatAttachmentsPreviewCurrentItemUnavailable.svelte';
|
import ChatAttachmentsPreviewCurrentItemUnavailable from './ChatAttachmentsPreviewCurrentItemUnavailable.svelte';
|
||||||
|
import ChatAttachmentsPreviewCurrentItemVideo from './ChatAttachmentsPreviewCurrentItemVideo.svelte';
|
||||||
|
import { FileIcon, FileText, Image, Music, Video } from '@lucide/svelte';
|
||||||
|
import type { ChatAttachmentDisplayItem } from '$lib/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentItem: ChatAttachmentDisplayItem | null;
|
currentItem: ChatAttachmentDisplayItem | null;
|
||||||
@@ -25,19 +25,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
activeModelId,
|
||||||
|
audioSrc,
|
||||||
currentItem,
|
currentItem,
|
||||||
isImage,
|
|
||||||
isAudio,
|
|
||||||
isVideo,
|
|
||||||
isPdf,
|
|
||||||
isText,
|
|
||||||
displayPreview,
|
displayPreview,
|
||||||
displayTextContent,
|
displayTextContent,
|
||||||
audioSrc,
|
|
||||||
videoSrc,
|
|
||||||
language,
|
|
||||||
hasVisionModality,
|
hasVisionModality,
|
||||||
activeModelId
|
isAudio,
|
||||||
|
isImage,
|
||||||
|
isPdf,
|
||||||
|
isText,
|
||||||
|
isVideo,
|
||||||
|
language,
|
||||||
|
videoSrc
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let IconComponent = $derived(
|
let IconComponent = $derived(
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
audioSrc: string | null;
|
audioSrc: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { currentItem, audioSrc }: Props = $props();
|
let { audioSrc, currentItem }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-center p-8">
|
<div class="flex flex-1 items-center justify-center p-8">
|
||||||
|
|||||||
+10
-7
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { Eye, FileText, Info } from '@lucide/svelte';
|
||||||
import type { ChatAttachmentDisplayItem } from '$lib/types';
|
|
||||||
import { FileText, Eye, Info } from '@lucide/svelte';
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import * as Alert from '$lib/components/ui/alert';
|
|
||||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||||
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
import { PdfViewMode } from '$lib/enums';
|
||||||
|
import type { ChatAttachmentDisplayItem } from '$lib/types';
|
||||||
import { getLanguageFromFilename } from '$lib/utils';
|
import { getLanguageFromFilename } from '$lib/utils';
|
||||||
import { convertPDFToImage } from '$lib/utils/browser-only';
|
import { convertPDFToImage } from '$lib/utils/browser-only';
|
||||||
import { PdfViewMode } from '$lib/enums';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentItem: ChatAttachmentDisplayItem | null;
|
currentItem: ChatAttachmentDisplayItem | null;
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
activeModelId?: string;
|
activeModelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { currentItem, displayName, displayTextContent, hasVisionModality, activeModelId }: Props =
|
let { activeModelId, currentItem, displayName, displayTextContent, hasVisionModality }: Props =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
let pdfViewMode = $state<PdfViewMode>(PdfViewMode.PAGES);
|
let pdfViewMode = $state<PdfViewMode>(PdfViewMode.PAGES);
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
currentItem.attachment.images.length > 0
|
currentItem.attachment.images.length > 0
|
||||||
) {
|
) {
|
||||||
pdfImages = currentItem.attachment.images;
|
pdfImages = currentItem.attachment.images;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +56,12 @@
|
|||||||
const base64Data = currentItem.attachment.base64Data;
|
const base64Data = currentItem.attachment.base64Data;
|
||||||
const byteCharacters = atob(base64Data);
|
const byteCharacters = atob(base64Data);
|
||||||
const byteNumbers = new Array(byteCharacters.length);
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
|
||||||
for (let i = 0; i < byteCharacters.length; i++) {
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
}
|
}
|
||||||
const byteArray = new Uint8Array(byteNumbers);
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
|
||||||
file = new File([byteArray], displayName, { type: 'application/pdf' });
|
file = new File([byteArray], displayName, { type: 'application/pdf' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onPrev, onNext, show }: Props = $props();
|
let { onNext, onPrev, show }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
|
|||||||
+5
-3
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { FileText, Music, Video } from '@lucide/svelte';
|
||||||
import { Music, Video, FileText } from '@lucide/svelte';
|
|
||||||
import { HorizontalScrollCarousel } from '$lib/components/app/misc';
|
import { HorizontalScrollCarousel } from '$lib/components/app/misc';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
|
||||||
interface PreviewItem {
|
interface PreviewItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,13 +18,15 @@
|
|||||||
onNavigate: (index: number) => void;
|
onNavigate: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { items, currentIndex, onNavigate }: Props = $props();
|
let { currentIndex, items, onNavigate }: Props = $props();
|
||||||
|
|
||||||
function getFileExtension(name: string): string {
|
function getFileExtension(name: string): string {
|
||||||
const parts = name.split('.');
|
const parts = name.split('.');
|
||||||
|
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
return parts.pop()?.toUpperCase() ?? '';
|
return parts.pop()?.toUpperCase() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ContextGaugePopup from './ChatFormContextGauge/ContextGaugePopup.svelte';
|
||||||
import {
|
import {
|
||||||
ChatAttachmentsList,
|
ChatAttachmentsList,
|
||||||
ChatFormActions,
|
ChatFormActions,
|
||||||
@@ -12,10 +13,10 @@
|
|||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import {
|
import {
|
||||||
CLIPBOARD_CONTENT_QUOTE_PREFIX,
|
CLIPBOARD_CONTENT_QUOTE_PREFIX,
|
||||||
INPUT_CLASSES,
|
|
||||||
SETTING_CONFIG_DEFAULT,
|
|
||||||
INITIAL_FILE_SIZE,
|
INITIAL_FILE_SIZE,
|
||||||
PROMPT_CONTENT_SEPARATOR
|
INPUT_CLASSES,
|
||||||
|
PROMPT_CONTENT_SEPARATOR,
|
||||||
|
SETTING_CONFIG_DEFAULT
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import {
|
import {
|
||||||
ContentPartType,
|
ContentPartType,
|
||||||
@@ -24,20 +25,20 @@
|
|||||||
MimeTypeText,
|
MimeTypeText,
|
||||||
SpecialFileType
|
SpecialFileType
|
||||||
} from '$lib/enums';
|
} from '$lib/enums';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte';
|
||||||
import ContextGaugePopup from './ChatFormContextGauge/ContextGaugePopup.svelte';
|
|
||||||
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
|
||||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
|
||||||
import { chatStore } from '$lib/stores/chat.svelte';
|
import { chatStore } from '$lib/stores/chat.svelte';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
|
||||||
import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import {
|
import {
|
||||||
conversationsStore,
|
|
||||||
activeMessages,
|
|
||||||
activeConversation,
|
activeConversation,
|
||||||
|
activeMessages,
|
||||||
|
conversationsStore,
|
||||||
pendingCwd
|
pendingCwd
|
||||||
} from '$lib/stores/conversations.svelte';
|
} from '$lib/stores/conversations.svelte';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
|
||||||
|
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||||
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||||
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
import type {
|
import type {
|
||||||
FileMentionEntry,
|
FileMentionEntry,
|
||||||
GetPromptResult,
|
GetPromptResult,
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
parseClipboardContent,
|
parseClipboardContent,
|
||||||
uuid
|
uuid
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte';
|
|
||||||
import {
|
import {
|
||||||
AudioRecorder,
|
AudioRecorder,
|
||||||
convertToWav,
|
convertToWav,
|
||||||
@@ -96,12 +96,6 @@
|
|||||||
class: className = '',
|
class: className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
placeholder = 'Type a message...',
|
|
||||||
showMcpPromptButton = false,
|
|
||||||
showAddButton = true,
|
|
||||||
showModelSelector = true,
|
|
||||||
uploadedFiles = $bindable([]),
|
|
||||||
value = $bindable(''),
|
|
||||||
onAttachmentRemove,
|
onAttachmentRemove,
|
||||||
onFilesAdd,
|
onFilesAdd,
|
||||||
onStop,
|
onStop,
|
||||||
@@ -109,7 +103,13 @@
|
|||||||
onSystemPromptClick,
|
onSystemPromptClick,
|
||||||
onUploadedFileRemove,
|
onUploadedFileRemove,
|
||||||
onUploadedFilesChange,
|
onUploadedFilesChange,
|
||||||
onValueChange
|
onValueChange,
|
||||||
|
placeholder = 'Type a message...',
|
||||||
|
showAddButton = true,
|
||||||
|
showMcpPromptButton = false,
|
||||||
|
showModelSelector = true,
|
||||||
|
uploadedFiles = $bindable([]),
|
||||||
|
value = $bindable('')
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Component References
|
// Component References
|
||||||
@@ -146,32 +146,35 @@
|
|||||||
let cwd = $derived(activeConversation()?.cwd ?? pendingCwd());
|
let cwd = $derived(activeConversation()?.cwd ?? pendingCwd());
|
||||||
|
|
||||||
const pickers = useChatFormPickers({
|
const pickers = useChatFormPickers({
|
||||||
|
focusInput: refocusInput,
|
||||||
|
getCaretOffset: () => inputRef?.getCaretOffset(),
|
||||||
|
getCwd: () => cwd,
|
||||||
|
getPickersRef: () => pickersRef,
|
||||||
|
getServerHome: () => toolsStore.serverHome ?? null,
|
||||||
|
getShowModelSelector: () => showModelSelector,
|
||||||
getValue: () => value,
|
getValue: () => value,
|
||||||
|
hasCwdTools: () => toolsStore.hasEnabledCwdTools,
|
||||||
|
hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()),
|
||||||
|
openModelSelector: () => chatFormActionsRef?.openModelSelector(),
|
||||||
|
setCaretOffset: (offset) => inputRef?.setCaretOffset(offset),
|
||||||
setValue: (v) => {
|
setValue: (v) => {
|
||||||
value = v;
|
value = v;
|
||||||
onValueChange?.(v);
|
onValueChange?.(v);
|
||||||
},
|
}
|
||||||
getCaretOffset: () => inputRef?.getCaretOffset(),
|
|
||||||
setCaretOffset: (offset) => inputRef?.setCaretOffset(offset),
|
|
||||||
focusInput: refocusInput,
|
|
||||||
getShowModelSelector: () => showModelSelector,
|
|
||||||
hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()),
|
|
||||||
hasCwdTools: () => toolsStore.hasEnabledCwdTools,
|
|
||||||
getCwd: () => cwd,
|
|
||||||
getServerHome: () => toolsStore.serverHome ?? null,
|
|
||||||
openModelSelector: () => chatFormActionsRef?.openModelSelector(),
|
|
||||||
getPickersRef: () => pickersRef
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleWorkingDirectoryChange(newDir: string | null) {
|
async function handleWorkingDirectoryChange(newDir: string | null) {
|
||||||
// Committing a directory consumes the `/cwd` token; the chip's
|
// Committing a directory consumes the `/cwd` token; the chip's
|
||||||
// clear-X path has no token to consume.
|
// clear-X path has no token to consume.
|
||||||
const token = findCommandToken(value);
|
const token = findCommandToken(value);
|
||||||
|
|
||||||
if (token && token.name === 'cwd') {
|
if (token && token.name === 'cwd') {
|
||||||
value = '';
|
value = '';
|
||||||
onValueChange?.('');
|
onValueChange?.('');
|
||||||
}
|
}
|
||||||
|
|
||||||
await conversationsStore.setCwd(newDir);
|
await conversationsStore.setCwd(newDir);
|
||||||
|
|
||||||
if (conversationsStore.activeConversation) {
|
if (conversationsStore.activeConversation) {
|
||||||
await chatStore.recordCwdChange(newDir?.trim() || null);
|
await chatStore.recordCwdChange(newDir?.trim() || null);
|
||||||
}
|
}
|
||||||
@@ -185,6 +188,7 @@
|
|||||||
|
|
||||||
let pasteLongTextToFileLength = $derived.by(() => {
|
let pasteLongTextToFileLength = $derived.by(() => {
|
||||||
const n = Number(currentConfig.pasteLongTextToFileLen);
|
const n = Number(currentConfig.pasteLongTextToFileLen);
|
||||||
|
|
||||||
return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n;
|
return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,13 +204,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedId = selectedModelId();
|
const selectedId = selectedModelId();
|
||||||
|
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
const model = options.find((m) => m.id === selectedId);
|
const model = options.find((m) => m.id === selectedId);
|
||||||
|
|
||||||
if (model) return model.model;
|
if (model) return model.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversationModel) {
|
if (conversationModel) {
|
||||||
const model = options.find((m) => m.model === conversationModel);
|
const model = options.find((m) => m.model === conversationModel);
|
||||||
|
|
||||||
if (model) return model.model;
|
if (model) return model.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +245,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const wantContenteditable =
|
const wantContenteditable =
|
||||||
containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
|
containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
|
||||||
|
|
||||||
if (useContenteditable === wantContenteditable) return;
|
if (useContenteditable === wantContenteditable) return;
|
||||||
|
|
||||||
if (!caretOffsetPinned) {
|
if (!caretOffsetPinned) {
|
||||||
@@ -268,8 +276,10 @@
|
|||||||
export function checkModelSelected(): boolean {
|
export function checkModelSelected(): boolean {
|
||||||
if (!hasModelSelected) {
|
if (!hasModelSelected) {
|
||||||
chatFormActionsRef?.openModelSelector();
|
chatFormActionsRef?.openModelSelector();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +294,7 @@
|
|||||||
function handleFileRemove(fileId: string) {
|
function handleFileRemove(fileId: string) {
|
||||||
if (fileId.startsWith('attachment-')) {
|
if (fileId.startsWith('attachment-')) {
|
||||||
const index = parseInt(fileId.replace('attachment-', ''), 10);
|
const index = parseInt(fileId.replace('attachment-', ''), 10);
|
||||||
|
|
||||||
if (!isNaN(index) && index >= 0 && index < attachments.length) {
|
if (!isNaN(index) && index >= 0 && index < attachments.length) {
|
||||||
onAttachmentRemove?.(index);
|
onAttachmentRemove?.(index);
|
||||||
}
|
}
|
||||||
@@ -333,6 +344,7 @@
|
|||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onFilesAdd?.(files);
|
onFilesAdd?.(files);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,26 +366,27 @@
|
|||||||
type: MimeTypeText.PLAIN
|
type: MimeTypeText.PLAIN
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
onFilesAdd?.(attachmentFiles);
|
onFilesAdd?.(attachmentFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle MCP prompt attachments as ChatUploadedFile with mcpPrompt data
|
// Handle MCP prompt attachments as ChatUploadedFile with mcpPrompt data
|
||||||
if (parsed.mcpPromptAttachments.length > 0) {
|
if (parsed.mcpPromptAttachments.length > 0) {
|
||||||
const mcpPromptFiles: ChatUploadedFile[] = parsed.mcpPromptAttachments.map((att) => ({
|
const mcpPromptFiles: ChatUploadedFile[] = parsed.mcpPromptAttachments.map((att) => ({
|
||||||
id: uuid(),
|
|
||||||
name: att.name,
|
|
||||||
size: att.content.length,
|
|
||||||
type: SpecialFileType.MCP_PROMPT,
|
|
||||||
file: new File([att.content], `${att.name}${FileExtensionText.TXT}`, {
|
file: new File([att.content], `${att.name}${FileExtensionText.TXT}`, {
|
||||||
type: MimeTypeText.PLAIN
|
type: MimeTypeText.PLAIN
|
||||||
}),
|
}),
|
||||||
|
id: uuid(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
textContent: att.content,
|
|
||||||
mcpPrompt: {
|
mcpPrompt: {
|
||||||
serverName: att.serverName,
|
arguments: att.arguments,
|
||||||
promptName: att.promptName,
|
promptName: att.promptName,
|
||||||
arguments: att.arguments
|
serverName: att.serverName
|
||||||
}
|
},
|
||||||
|
name: att.name,
|
||||||
|
size: att.content.length,
|
||||||
|
textContent: att.content,
|
||||||
|
type: SpecialFileType.MCP_PROMPT
|
||||||
}));
|
}));
|
||||||
|
|
||||||
uploadedFiles = [...uploadedFiles, ...mcpPromptFiles];
|
uploadedFiles = [...uploadedFiles, ...mcpPromptFiles];
|
||||||
@@ -412,17 +425,17 @@
|
|||||||
|
|
||||||
const promptName = promptInfo.title || promptInfo.name;
|
const promptName = promptInfo.title || promptInfo.name;
|
||||||
const placeholder: ChatUploadedFile = {
|
const placeholder: ChatUploadedFile = {
|
||||||
id: placeholderId,
|
|
||||||
name: promptName,
|
|
||||||
size: INITIAL_FILE_SIZE,
|
|
||||||
type: SpecialFileType.MCP_PROMPT,
|
|
||||||
file: new File([], 'loading'),
|
file: new File([], 'loading'),
|
||||||
|
id: placeholderId,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
mcpPrompt: {
|
mcpPrompt: {
|
||||||
serverName: promptInfo.serverName,
|
arguments: args ? { ...args } : undefined,
|
||||||
promptName: promptInfo.name,
|
promptName: promptInfo.name,
|
||||||
arguments: args ? { ...args } : undefined
|
serverName: promptInfo.serverName
|
||||||
}
|
},
|
||||||
|
name: promptName,
|
||||||
|
size: INITIAL_FILE_SIZE,
|
||||||
|
type: SpecialFileType.MCP_PROMPT
|
||||||
};
|
};
|
||||||
|
|
||||||
uploadedFiles = [...uploadedFiles, placeholder];
|
uploadedFiles = [...uploadedFiles, placeholder];
|
||||||
@@ -450,12 +463,12 @@
|
|||||||
f.id === placeholderId
|
f.id === placeholderId
|
||||||
? {
|
? {
|
||||||
...f,
|
...f,
|
||||||
isLoading: false,
|
|
||||||
textContent: promptText,
|
|
||||||
size: promptText.length,
|
|
||||||
file: new File([promptText], `${f.name}${FileExtensionText.TXT}`, {
|
file: new File([promptText], `${f.name}${FileExtensionText.TXT}`, {
|
||||||
type: MimeTypeText.PLAIN
|
type: MimeTypeText.PLAIN
|
||||||
})
|
}),
|
||||||
|
isLoading: false,
|
||||||
|
size: promptText.length,
|
||||||
|
textContent: promptText
|
||||||
}
|
}
|
||||||
: f
|
: f
|
||||||
);
|
);
|
||||||
@@ -480,9 +493,11 @@
|
|||||||
function handleMentionSelect(entry: FileMentionEntry) {
|
function handleMentionSelect(entry: FileMentionEntry) {
|
||||||
const cursor = inputRef?.getCaretOffset() ?? value.length;
|
const cursor = inputRef?.getCaretOffset() ?? value.length;
|
||||||
const token = findMentionToken(value, cursor);
|
const token = findMentionToken(value, cursor);
|
||||||
|
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const built = buildMentionInsertion(entry, value, token);
|
const built = buildMentionInsertion(entry, value, token);
|
||||||
|
|
||||||
if (!built) return;
|
if (!built) return;
|
||||||
|
|
||||||
// Pin the post-insertion caret BEFORE the swap effect runs;
|
// Pin the post-insertion caret BEFORE the swap effect runs;
|
||||||
@@ -504,6 +519,7 @@
|
|||||||
async function handleMicClick() {
|
async function handleMicClick() {
|
||||||
if (!audioRecorder || !recordingSupported) {
|
if (!audioRecorder || !recordingSupported) {
|
||||||
console.warn('Audio recording not supported');
|
console.warn('Audio recording not supported');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +658,7 @@
|
|||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
onMicClick={handleMicClick}
|
onMicClick={handleMicClick}
|
||||||
{onStop}
|
{onStop}
|
||||||
onSystemPromptClick={() => onSystemPromptClick?.({ message: value, files: uploadedFiles })}
|
onSystemPromptClick={() => onSystemPromptClick?.({ files: uploadedFiles, message: value })}
|
||||||
onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined}
|
onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined}
|
||||||
onMcpResourcesClick={() => (isResourceDialogOpen = true)}
|
onMcpResourcesClick={() => (isResourceDialogOpen = true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
|
||||||
import { Plus } from '@lucide/svelte';
|
import { Plus } from '@lucide/svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants';
|
import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
+16
-16
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { File, FolderOpen, MessageSquare, Plus, Zap } from '@lucide/svelte';
|
||||||
import { Plus, File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
|
import {
|
||||||
|
ChatFormActionAddMcpServersSubmenu,
|
||||||
|
ChatFormActionAddReasoningSubmenu,
|
||||||
|
ChatFormActionAddToolsSubmenu
|
||||||
|
} from '$lib/components/app';
|
||||||
|
import { buttonVariants } from '$lib/components/ui/button';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { buttonVariants } from '$lib/components/ui/button';
|
|
||||||
import { cn } from '$lib/components/ui/utils';
|
import { cn } from '$lib/components/ui/utils';
|
||||||
import {
|
import {
|
||||||
ATTACHMENT_FILE_ITEMS,
|
ATTACHMENT_FILE_ITEMS,
|
||||||
ATTACHMENT_TOOLTIP_TEXT,
|
ATTACHMENT_TOOLTIP_TEXT,
|
||||||
TOOLTIP_DELAY_DURATION
|
TOOLTIP_DELAY_DURATION
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import {
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
ChatFormActionAddToolsSubmenu,
|
|
||||||
ChatFormActionAddMcpServersSubmenu,
|
|
||||||
ChatFormActionAddReasoningSubmenu
|
|
||||||
} from '$lib/components/app';
|
|
||||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -36,15 +36,15 @@
|
|||||||
class: className = '',
|
class: className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
hasAudioModality = false,
|
hasAudioModality = false,
|
||||||
hasVideoModality = false,
|
|
||||||
hasVisionModality = false,
|
|
||||||
hasMcpPromptsSupport = false,
|
hasMcpPromptsSupport = false,
|
||||||
hasMcpResourcesSupport = false,
|
hasMcpResourcesSupport = false,
|
||||||
|
hasVideoModality = false,
|
||||||
|
hasVisionModality = false,
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onSystemPromptClick,
|
|
||||||
onMcpPromptClick,
|
onMcpPromptClick,
|
||||||
|
onMcpResourcesClick,
|
||||||
onMcpSettingsClick,
|
onMcpSettingsClick,
|
||||||
onMcpResourcesClick
|
onSystemPromptClick
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let dropdownOpen = $state(false);
|
let dropdownOpen = $state(false);
|
||||||
@@ -59,13 +59,13 @@
|
|||||||
|
|
||||||
const attachmentMenu = useAttachmentMenu(
|
const attachmentMenu = useAttachmentMenu(
|
||||||
() => ({
|
() => ({
|
||||||
hasVisionModality,
|
|
||||||
hasAudioModality,
|
hasAudioModality,
|
||||||
hasVideoModality,
|
|
||||||
hasMcpPromptsSupport,
|
hasMcpPromptsSupport,
|
||||||
hasMcpResourcesSupport
|
hasMcpResourcesSupport,
|
||||||
|
hasVideoModality,
|
||||||
|
hasVisionModality
|
||||||
}),
|
}),
|
||||||
() => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
|
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
||||||
() => {
|
() => {
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { Plus, Settings } from '@lucide/svelte';
|
||||||
import { Settings, Plus } from '@lucide/svelte';
|
import { goto } from '$app/navigation';
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
import { DropdownMenuSearchable, McpLogo, McpServerIdentity } from '$lib/components/app';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import { McpLogo, DropdownMenuSearchable, McpServerIdentity } from '$lib/components/app';
|
import { Switch } from '$lib/components/ui/switch';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
import { HealthCheckStatus } from '$lib/enums';
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
import { HealthCheckStatus } from '$lib/enums';
|
|
||||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { ROUTES } from '$lib/constants/routes';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onMcpSettingsClick?: () => void;
|
onMcpSettingsClick?: () => void;
|
||||||
@@ -24,10 +24,13 @@
|
|||||||
let hasMcpServers = $derived(mcpServers.length > 0);
|
let hasMcpServers = $derived(mcpServers.length > 0);
|
||||||
let filteredMcpServers = $derived.by(() => {
|
let filteredMcpServers = $derived.by(() => {
|
||||||
const query = mcpSearchQuery.toLowerCase().trim();
|
const query = mcpSearchQuery.toLowerCase().trim();
|
||||||
|
|
||||||
if (!query) return mcpServers;
|
if (!query) return mcpServers;
|
||||||
|
|
||||||
return mcpServers.filter((s) => {
|
return mcpServers.filter((s) => {
|
||||||
const name = getServerLabel(s).toLowerCase();
|
const name = getServerLabel(s).toLowerCase();
|
||||||
const url = s.url.toLowerCase();
|
const url = s.url.toLowerCase();
|
||||||
|
|
||||||
return name.includes(query) || url.includes(query);
|
return name.includes(query) || url.includes(query);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { Check, Info, Lightbulb, LightbulbOff } from '@lucide/svelte';
|
||||||
import { Lightbulb, LightbulbOff, Check, Info } from '@lucide/svelte';
|
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
||||||
|
|
||||||
const reasoning = useReasoningMenu();
|
const reasoning = useReasoningMenu();
|
||||||
|
|||||||
+25
-25
@@ -1,30 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { File, FolderOpen, MessageSquare, Zap } from '@lucide/svelte';
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
|
||||||
import * as Sheet from '$lib/components/ui/sheet';
|
|
||||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
|
||||||
import { File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
|
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
|
||||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
|
|
||||||
import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
|
|
||||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
|
||||||
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
|
|
||||||
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
|
||||||
import { McpLogo } from '$lib/components/app';
|
|
||||||
import {
|
import {
|
||||||
PencilRuler,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
LightbulbOff,
|
LightbulbOff,
|
||||||
Check
|
PencilRuler
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
|
import { McpLogo } from '$lib/components/app';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
|
import * as Sheet from '$lib/components/ui/sheet';
|
||||||
|
import { Switch } from '$lib/components/ui/switch';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
|
||||||
|
import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
import { HealthCheckStatus } from '$lib/enums';
|
import { HealthCheckStatus } from '$lib/enums';
|
||||||
import { AttachmentAction } from '$lib/enums/attachment.enums';
|
import { AttachmentAction } from '$lib/enums/attachment.enums';
|
||||||
|
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||||
|
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
||||||
|
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
|
||||||
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -45,14 +45,14 @@
|
|||||||
class: className = '',
|
class: className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
hasAudioModality = false,
|
hasAudioModality = false,
|
||||||
hasVisionModality = false,
|
|
||||||
hasVideoModality = false,
|
|
||||||
hasMcpPromptsSupport = false,
|
hasMcpPromptsSupport = false,
|
||||||
hasMcpResourcesSupport = false,
|
hasMcpResourcesSupport = false,
|
||||||
|
hasVideoModality = false,
|
||||||
|
hasVisionModality = false,
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onSystemPromptClick,
|
|
||||||
onMcpPromptClick,
|
onMcpPromptClick,
|
||||||
onMcpResourcesClick,
|
onMcpResourcesClick,
|
||||||
|
onSystemPromptClick,
|
||||||
trigger
|
trigger
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@
|
|||||||
|
|
||||||
const attachmentMenu = useAttachmentMenu(
|
const attachmentMenu = useAttachmentMenu(
|
||||||
() => ({
|
() => ({
|
||||||
hasVisionModality,
|
|
||||||
hasAudioModality,
|
hasAudioModality,
|
||||||
hasVideoModality,
|
|
||||||
hasMcpPromptsSupport,
|
hasMcpPromptsSupport,
|
||||||
hasMcpResourcesSupport
|
hasMcpResourcesSupport,
|
||||||
|
hasVideoModality,
|
||||||
|
hasVisionModality
|
||||||
}),
|
}),
|
||||||
() => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
|
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
||||||
() => {
|
() => {
|
||||||
sheetOpen = false;
|
sheetOpen = false;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { Check, ChevronDown, ChevronRight, Info, Loader2, PencilRuler } from '@lucide/svelte';
|
||||||
import { PencilRuler, ChevronDown, ChevronRight, Loader2, Info, Check } from '@lucide/svelte';
|
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import { CLI_FLAGS } from '$lib/constants';
|
import { CLI_FLAGS } from '$lib/constants';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
|
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
|
||||||
const toolsPanel = useToolsPanel();
|
const toolsPanel = useToolsPanel();
|
||||||
const hasMcpServersAvailable = $derived(mcpStore.getServers().length > 0);
|
const hasMcpServersAvailable = $derived(mcpStore.getServers().length > 0);
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
|
||||||
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
|
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
|
||||||
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
|
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
|
||||||
import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
|
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
let {
|
let {
|
||||||
disabled = false,
|
disabled = false,
|
||||||
hasAudioModality = false,
|
hasAudioModality = false,
|
||||||
hasVideoModality = false,
|
|
||||||
hasMcpPromptsSupport = false,
|
hasMcpPromptsSupport = false,
|
||||||
hasMcpResourcesSupport = false,
|
hasMcpResourcesSupport = false,
|
||||||
|
hasVideoModality = false,
|
||||||
hasVisionModality = false,
|
hasVisionModality = false,
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onMcpPromptClick,
|
onMcpPromptClick,
|
||||||
|
|||||||
+7
-4
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { ModelsSelectorDropdown, ModelsSelectorSheet } from '$lib/components/app';
|
||||||
import { chatStore } from '$lib/stores/chat.svelte';
|
import { chatStore } from '$lib/stores/chat.svelte';
|
||||||
|
import { activeMessages } from '$lib/stores/conversations.svelte';
|
||||||
import {
|
import {
|
||||||
modelsStore,
|
|
||||||
modelOptions,
|
modelOptions,
|
||||||
|
modelsStore,
|
||||||
selectedModelId,
|
selectedModelId,
|
||||||
selectedModelName
|
selectedModelName
|
||||||
} from '$lib/stores/models.svelte';
|
} from '$lib/stores/models.svelte';
|
||||||
import { isRouterMode, serverError } from '$lib/stores/server.svelte';
|
import { isRouterMode, serverError } from '$lib/stores/server.svelte';
|
||||||
import { ModelsSelectorDropdown, ModelsSelectorSheet } from '$lib/components/app';
|
|
||||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||||
import { activeMessages } from '$lib/stores/conversations.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -27,9 +27,9 @@
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
forceForegroundText = false,
|
forceForegroundText = false,
|
||||||
hasAudioModality = $bindable(false),
|
hasAudioModality = $bindable(false),
|
||||||
|
hasModelSelected = $bindable(false),
|
||||||
hasVideoModality = $bindable(false),
|
hasVideoModality = $bindable(false),
|
||||||
hasVisionModality = $bindable(false),
|
hasVisionModality = $bindable(false),
|
||||||
hasModelSelected = $bindable(false),
|
|
||||||
isSelectedModelInCache = $bindable(true),
|
isSelectedModelInCache = $bindable(true),
|
||||||
submitTooltip = $bindable(''),
|
submitTooltip = $bindable(''),
|
||||||
useGlobalSelection = false
|
useGlobalSelection = false
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
let selectorModel = $derived.by(() => {
|
let selectorModel = $derived.by(() => {
|
||||||
const storeModel = selectedModelName();
|
const storeModel = selectedModelName();
|
||||||
|
|
||||||
if (storeModel && storeModel !== conversationModel) {
|
if (storeModel && storeModel !== conversationModel) {
|
||||||
return storeModel;
|
return storeModel;
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
modelsStore.selectedModelName = null;
|
modelsStore.selectedModelName = null;
|
||||||
modelsStore.clearSelection();
|
modelsStore.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSyncedConversationModel = conversationModel;
|
lastSyncedConversationModel = conversationModel;
|
||||||
} else if (
|
} else if (
|
||||||
isRouter &&
|
isRouter &&
|
||||||
@@ -76,6 +78,7 @@
|
|||||||
) {
|
) {
|
||||||
lastSyncedConversationModel = null;
|
lastSyncedConversationModel = null;
|
||||||
const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
|
const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
|
||||||
|
|
||||||
if (first) modelsStore.selectModelById(first.id);
|
if (first) modelsStore.selectModelById(first.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
|
||||||
import { Mic, Square } from '@lucide/svelte';
|
import { Mic, Square } from '@lucide/svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
+26
-16
@@ -1,28 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { SkipForward, Square } from '@lucide/svelte';
|
||||||
import { Square, SkipForward } from '@lucide/svelte';
|
import { goto } from '$app/navigation';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { page } from '$app/state';
|
||||||
import { ChatService } from '$lib/services';
|
|
||||||
import {
|
import {
|
||||||
ChatFormActionsAdd,
|
|
||||||
ChatFormActionModels,
|
ChatFormActionModels,
|
||||||
ChatFormActionRecord,
|
ChatFormActionRecord,
|
||||||
|
ChatFormActionsAdd,
|
||||||
ChatFormActionSubmit,
|
ChatFormActionSubmit,
|
||||||
ChatFormContextGauge
|
ChatFormContextGauge
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { ChatService } from '$lib/services';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
|
||||||
import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
|
|
||||||
import {
|
import {
|
||||||
activeProcessingState,
|
activeProcessingState,
|
||||||
isChatStreaming,
|
isChatStreaming,
|
||||||
isLoading as chatIsLoading
|
isLoading as chatIsLoading
|
||||||
} from '$lib/stores/chat.svelte';
|
} from '$lib/stores/chat.svelte';
|
||||||
|
import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { getFileTypeCategory } from '$lib/utils';
|
import { getFileTypeCategory } from '$lib/utils';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { page } from '$app/state';
|
|
||||||
import { ROUTES } from '$lib/constants/routes';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canSend?: boolean;
|
canSend?: boolean;
|
||||||
@@ -51,15 +51,15 @@
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
isReasoning = false,
|
isReasoning = false,
|
||||||
isRecording = false,
|
isRecording = false,
|
||||||
showAddButton = true,
|
|
||||||
showModelSelector = true,
|
|
||||||
uploadedFiles = [],
|
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
|
onMcpPromptClick,
|
||||||
|
onMcpResourcesClick,
|
||||||
onMicClick,
|
onMicClick,
|
||||||
onStop,
|
onStop,
|
||||||
onSystemPromptClick,
|
onSystemPromptClick,
|
||||||
onMcpPromptClick,
|
showAddButton = true,
|
||||||
onMcpResourcesClick
|
showModelSelector = true,
|
||||||
|
uploadedFiles = []
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let currentConfig = $derived(config());
|
let currentConfig = $derived(config());
|
||||||
@@ -105,29 +105,39 @@
|
|||||||
if (!page.params.id) return false;
|
if (!page.params.id) return false;
|
||||||
|
|
||||||
const messages = activeMessages() as DatabaseMessage[];
|
const messages = activeMessages() as DatabaseMessage[];
|
||||||
|
|
||||||
let totalHistoricalTokens = 0;
|
let totalHistoricalTokens = 0;
|
||||||
|
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
if (m.role !== MessageRole.ASSISTANT) continue;
|
if (m.role !== MessageRole.ASSISTANT) continue;
|
||||||
|
|
||||||
const timings = m.timings;
|
const timings = m.timings;
|
||||||
|
|
||||||
if (!timings) continue;
|
if (!timings) continue;
|
||||||
|
|
||||||
const agenticLlm = timings.agentic?.llm;
|
const agenticLlm = timings.agentic?.llm;
|
||||||
|
|
||||||
if (agenticLlm?.prompt_n != null || agenticLlm?.predicted_n != null) {
|
if (agenticLlm?.prompt_n != null || agenticLlm?.predicted_n != null) {
|
||||||
totalHistoricalTokens += (agenticLlm?.prompt_n ?? 0) + (agenticLlm?.predicted_n ?? 0);
|
totalHistoricalTokens += (agenticLlm?.prompt_n ?? 0) + (agenticLlm?.predicted_n ?? 0);
|
||||||
} else {
|
} else {
|
||||||
totalHistoricalTokens += (timings.prompt_n ?? 0) + (timings.predicted_n ?? 0);
|
totalHistoricalTokens += (timings.prompt_n ?? 0) + (timings.predicted_n ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalHistoricalTokens > 0) return true;
|
if (totalHistoricalTokens > 0) return true;
|
||||||
|
|
||||||
if (!chatIsLoading() && !isChatStreaming()) return false;
|
if (!chatIsLoading() && !isChatStreaming()) return false;
|
||||||
|
|
||||||
const processingState = activeProcessingState();
|
const processingState = activeProcessingState();
|
||||||
|
|
||||||
if (!processingState) return false;
|
if (!processingState) return false;
|
||||||
|
|
||||||
const livePromptTokens = Math.max(
|
const livePromptTokens = Math.max(
|
||||||
processingState.promptTokens ?? 0,
|
processingState.promptTokens ?? 0,
|
||||||
processingState.promptProgress?.processed ?? 0
|
processingState.promptProgress?.processed ?? 0
|
||||||
);
|
);
|
||||||
const liveOutputTokens = processingState.outputTokensUsed ?? 0;
|
const liveOutputTokens = processingState.outputTokensUsed ?? 0;
|
||||||
|
|
||||||
return livePromptTokens > 0 || liveOutputTokens > 0;
|
return livePromptTokens > 0 || liveOutputTokens > 0;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, untrack } from 'svelte';
|
|
||||||
import { mode } from 'mode-watcher';
|
|
||||||
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
|
||||||
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
|
||||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
|
||||||
import { ColorMode } from '$lib/enums';
|
|
||||||
import { TRIM_LEADING_PADDING_REGEX, TRIM_TRAILING_PADDING_REGEX } from '$lib/constants';
|
import { TRIM_LEADING_PADDING_REGEX, TRIM_TRAILING_PADDING_REGEX } from '$lib/constants';
|
||||||
|
import { ColorMode } from '$lib/enums';
|
||||||
|
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||||
|
import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
|
||||||
import {
|
import {
|
||||||
badgeAwareWordJump,
|
badgeAwareWordJump,
|
||||||
buildFragment,
|
buildFragment,
|
||||||
@@ -19,10 +16,13 @@
|
|||||||
SourceHistory,
|
SourceHistory,
|
||||||
stripBlockBoundaryLineBreaks,
|
stripBlockBoundaryLineBreaks,
|
||||||
syncCodeBlockHatches,
|
syncCodeBlockHatches,
|
||||||
tokenizeContent,
|
textOffsetToRange,
|
||||||
textOffsetToRange
|
tokenizeContent
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
|
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
||||||
|
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
import { onDestroy, onMount, untrack } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -57,7 +57,9 @@
|
|||||||
// serialized source, not the DOM shape.
|
// serialized source, not the DOM shape.
|
||||||
function syncEmptyState(serialized?: string) {
|
function syncEmptyState(serialized?: string) {
|
||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
const source = serialized ?? serializeContent(rootElement);
|
const source = serialized ?? serializeContent(rootElement);
|
||||||
|
|
||||||
rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
|
rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,23 +95,24 @@
|
|||||||
*/
|
*/
|
||||||
function highlightCodeBlockElement(el: HTMLElement): boolean {
|
function highlightCodeBlockElement(el: HTMLElement): boolean {
|
||||||
const segment = el.textContent ?? '';
|
const segment = el.textContent ?? '';
|
||||||
|
|
||||||
if (highlightedSegments.get(el) === segment) return false;
|
if (highlightedSegments.get(el) === segment) return false;
|
||||||
|
|
||||||
const open = CODE_BLOCK_OPEN_RE.exec(segment);
|
const open = CODE_BLOCK_OPEN_RE.exec(segment);
|
||||||
|
|
||||||
if (!open) return false;
|
if (!open) return false;
|
||||||
|
|
||||||
const prefix = open[0];
|
const prefix = open[0];
|
||||||
const language = open[1].trim().split(/\s+/)[0] ?? '';
|
const language = open[1].trim().split(/\s+/)[0] ?? '';
|
||||||
const content = segment.slice(prefix.length, -3);
|
const content = segment.slice(prefix.length, -3);
|
||||||
|
|
||||||
const leading = content.match(TRIM_LEADING_PADDING_REGEX)?.[0] ?? '';
|
const leading = content.match(TRIM_LEADING_PADDING_REGEX)?.[0] ?? '';
|
||||||
const trailing = content.match(TRIM_TRAILING_PADDING_REGEX)?.[0] ?? '';
|
const trailing = content.match(TRIM_TRAILING_PADDING_REGEX)?.[0] ?? '';
|
||||||
const core = content.slice(leading.length, content.length - trailing.length);
|
const core = content.slice(leading.length, content.length - trailing.length);
|
||||||
|
|
||||||
// autoDetect off: re-guessing the language on every keystroke
|
// autoDetect off: re-guessing the language on every keystroke
|
||||||
// costs ~38ms a call and flickers while typing
|
// costs ~38ms a call and flickers while typing
|
||||||
const html = core ? highlightCode(core, language || 'text', false) : '';
|
const html = core ? highlightCode(core, language || 'text', false) : '';
|
||||||
const tpl = document.createElement('template');
|
const tpl = document.createElement('template');
|
||||||
|
|
||||||
tpl.innerHTML = html;
|
tpl.innerHTML = html;
|
||||||
|
|
||||||
el.replaceChildren(
|
el.replaceChildren(
|
||||||
@@ -118,6 +121,7 @@
|
|||||||
document.createTextNode(trailing + '```')
|
document.createTextNode(trailing + '```')
|
||||||
);
|
);
|
||||||
highlightedSegments.set(el, segment);
|
highlightedSegments.set(el, segment);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +140,11 @@
|
|||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
const range = safeRange();
|
const range = safeRange();
|
||||||
|
|
||||||
if (!range) return;
|
if (!range) return;
|
||||||
|
|
||||||
let node: Node | null = range.startContainer;
|
let node: Node | null = range.startContainer;
|
||||||
|
|
||||||
if (node === rootElement) {
|
if (node === rootElement) {
|
||||||
node = rootElement.childNodes[range.startOffset - 1] ?? null;
|
node = rootElement.childNodes[range.startOffset - 1] ?? null;
|
||||||
}
|
}
|
||||||
@@ -146,11 +152,14 @@
|
|||||||
while (node && node !== rootElement) {
|
while (node && node !== rootElement) {
|
||||||
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
|
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
|
||||||
const caret = rangeToTextOffset(rootElement, range);
|
const caret = rangeToTextOffset(rootElement, range);
|
||||||
|
|
||||||
if (highlightCodeBlockElement(node)) {
|
if (highlightCodeBlockElement(node)) {
|
||||||
restoreCaret(caret);
|
restoreCaret(caret);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
node = node.parentNode;
|
node = node.parentNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +191,7 @@
|
|||||||
document.querySelectorAll('style[data-highlight-theme-preview]').forEach((s) => s.remove());
|
document.querySelectorAll('style[data-highlight-theme-preview]').forEach((s) => s.remove());
|
||||||
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
|
|
||||||
style.setAttribute('data-highlight-theme-preview', 'true');
|
style.setAttribute('data-highlight-theme-preview', 'true');
|
||||||
style.textContent = isDark ? githubDarkCss : githubLightCss;
|
style.textContent = isDark ? githubDarkCss : githubLightCss;
|
||||||
|
|
||||||
@@ -196,6 +206,7 @@
|
|||||||
if (!rootElement) return null;
|
if (!rootElement) return null;
|
||||||
|
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
if (!selection || selection.rangeCount === 0) return null;
|
if (!selection || selection.rangeCount === 0) return null;
|
||||||
|
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
@@ -212,6 +223,7 @@
|
|||||||
|
|
||||||
const target = textOffsetToRange(rootElement, offset);
|
const target = textOffsetToRange(rootElement, offset);
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
if (!selection) return;
|
if (!selection) return;
|
||||||
|
|
||||||
if (extend && selection.anchorNode) {
|
if (extend && selection.anchorNode) {
|
||||||
@@ -221,6 +233,7 @@
|
|||||||
target.startContainer,
|
target.startContainer,
|
||||||
target.startOffset
|
target.startOffset
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,14 +243,16 @@
|
|||||||
|
|
||||||
function resizeHeight() {
|
function resizeHeight() {
|
||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
rootElement.style.height = 'auto';
|
rootElement.style.height = 'auto';
|
||||||
rootElement.style.height = `${rootElement.scrollHeight}px`;
|
rootElement.style.height = `${rootElement.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordHistory(newGroup: boolean) {
|
function recordHistory(newGroup: boolean) {
|
||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
{ value: lastEmittedValue, caret: rangeToTextOffset(rootElement, safeRange()) },
|
{ caret: rangeToTextOffset(rootElement, safeRange()), value: lastEmittedValue },
|
||||||
Date.now(),
|
Date.now(),
|
||||||
newGroup
|
newGroup
|
||||||
);
|
);
|
||||||
@@ -262,10 +277,12 @@
|
|||||||
// lands on the line directly below the block.
|
// lands on the line directly below the block.
|
||||||
if (inputType === 'insertLineBreak' || inputType === 'insertParagraph') {
|
if (inputType === 'insertLineBreak' || inputType === 'insertParagraph') {
|
||||||
const caret = rangeToTextOffset(rootElement, safeRange());
|
const caret = rangeToTextOffset(rootElement, safeRange());
|
||||||
|
|
||||||
if (stripBlockBoundaryLineBreaks(rootElement)) {
|
if (stripBlockBoundaryLineBreaks(rootElement)) {
|
||||||
restoreCaret(caret);
|
restoreCaret(caret);
|
||||||
} else {
|
} else {
|
||||||
const source = serializeContent(rootElement);
|
const source = serializeContent(rootElement);
|
||||||
|
|
||||||
let end = caret;
|
let end = caret;
|
||||||
|
|
||||||
// the caret must end up after the inserted \n; some browsers
|
// the caret must end up after the inserted \n; some browsers
|
||||||
@@ -284,7 +301,9 @@
|
|||||||
// \n doubles as a block's separator line (source ends with
|
// \n doubles as a block's separator line (source ends with
|
||||||
// \n\n) or sits inside a block element.
|
// \n\n) or sits inside a block element.
|
||||||
let last = rootElement.lastChild;
|
let last = rootElement.lastChild;
|
||||||
|
|
||||||
while (last && last.nodeName === 'BR') last = last.previousSibling;
|
while (last && last.nodeName === 'BR') last = last.previousSibling;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
end === source.length &&
|
end === source.length &&
|
||||||
source.endsWith('\n') &&
|
source.endsWith('\n') &&
|
||||||
@@ -302,7 +321,9 @@
|
|||||||
syncCodeBlockHatches(rootElement);
|
syncCodeBlockHatches(rootElement);
|
||||||
|
|
||||||
const serialized = serializeContent(rootElement);
|
const serialized = serializeContent(rootElement);
|
||||||
|
|
||||||
syncEmptyState(serialized);
|
syncEmptyState(serialized);
|
||||||
|
|
||||||
if (serialized === lastEmittedValue) return;
|
if (serialized === lastEmittedValue) return;
|
||||||
|
|
||||||
// Plain typing/deletes coalesce per time window; structural edits
|
// Plain typing/deletes coalesce per time window; structural edits
|
||||||
@@ -316,6 +337,7 @@
|
|||||||
// completed or broken) - the browser-owned text nodes cannot
|
// completed or broken) - the browser-owned text nodes cannot
|
||||||
// restyle themselves across element boundaries.
|
// restyle themselves across element boundaries.
|
||||||
const tokens = tokenizeContent(serialized);
|
const tokens = tokenizeContent(serialized);
|
||||||
|
|
||||||
if (!domMatchesTokens(rootElement, tokens)) {
|
if (!domMatchesTokens(rootElement, tokens)) {
|
||||||
renderTokens(tokens);
|
renderTokens(tokens);
|
||||||
|
|
||||||
@@ -324,6 +346,7 @@
|
|||||||
// block element and the rebuild splits it back out, which
|
// block element and the rebuild splits it back out, which
|
||||||
// synthesizes the separator newline) - keep value in sync.
|
// synthesizes the separator newline) - keep value in sync.
|
||||||
const reserialized = serializeContent(rootElement);
|
const reserialized = serializeContent(rootElement);
|
||||||
|
|
||||||
if (reserialized !== serialized) {
|
if (reserialized !== serialized) {
|
||||||
lastEmittedValue = reserialized;
|
lastEmittedValue = reserialized;
|
||||||
value = reserialized;
|
value = reserialized;
|
||||||
@@ -361,6 +384,7 @@
|
|||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
const range = safeRange();
|
const range = safeRange();
|
||||||
|
|
||||||
if (!range) return;
|
if (!range) return;
|
||||||
|
|
||||||
if (!range.collapsed) {
|
if (!range.collapsed) {
|
||||||
@@ -374,16 +398,22 @@
|
|||||||
// a break at the very end of a code block exits the block (the
|
// a break at the very end of a code block exits the block (the
|
||||||
// new line belongs below it, not inside)
|
// new line belongs below it, not inside)
|
||||||
let exitBlock: HTMLElement | null = null;
|
let exitBlock: HTMLElement | null = null;
|
||||||
|
|
||||||
if (container.nodeType === Node.TEXT_NODE) {
|
if (container.nodeType === Node.TEXT_NODE) {
|
||||||
let node: Node | null = container.parentNode;
|
let node: Node | null = container.parentNode;
|
||||||
|
|
||||||
while (node && node !== rootElement) {
|
while (node && node !== rootElement) {
|
||||||
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
|
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
|
||||||
const tail = document.createRange();
|
const tail = document.createRange();
|
||||||
|
|
||||||
tail.setStart(container, offset);
|
tail.setStart(container, offset);
|
||||||
tail.setEnd(node, node.childNodes.length);
|
tail.setEnd(node, node.childNodes.length);
|
||||||
|
|
||||||
if (tail.toString().length === 0) exitBlock = node;
|
if (tail.toString().length === 0) exitBlock = node;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
node = node.parentNode;
|
node = node.parentNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,6 +422,7 @@
|
|||||||
exitBlock.after(nl);
|
exitBlock.after(nl);
|
||||||
} else if (container.nodeType === Node.TEXT_NODE) {
|
} else if (container.nodeType === Node.TEXT_NODE) {
|
||||||
const text = container as Text;
|
const text = container as Text;
|
||||||
|
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
text.before(nl);
|
text.before(nl);
|
||||||
} else if (offset === text.length) {
|
} else if (offset === text.length) {
|
||||||
@@ -405,6 +436,7 @@
|
|||||||
|
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
const after = document.createRange();
|
const after = document.createRange();
|
||||||
|
|
||||||
after.setStartAfter(nl);
|
after.setStartAfter(nl);
|
||||||
after.collapse(true);
|
after.collapse(true);
|
||||||
selection?.removeAllRanges();
|
selection?.removeAllRanges();
|
||||||
@@ -428,9 +460,11 @@
|
|||||||
if (rootElement.firstChild?.nodeName === 'BR') return false;
|
if (rootElement.firstChild?.nodeName === 'BR') return false;
|
||||||
|
|
||||||
const first = rootElement.firstChild;
|
const first = rootElement.firstChild;
|
||||||
|
|
||||||
if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false;
|
if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false;
|
||||||
|
|
||||||
const range = safeRange();
|
const range = safeRange();
|
||||||
|
|
||||||
if (!range || !range.collapsed) return false;
|
if (!range || !range.collapsed) return false;
|
||||||
|
|
||||||
// the caret must sit inside the block: on its very first
|
// the caret must sit inside the block: on its very first
|
||||||
@@ -439,16 +473,19 @@
|
|||||||
if (!first.contains(range.startContainer)) return false;
|
if (!first.contains(range.startContainer)) return false;
|
||||||
|
|
||||||
const caret = rangeToTextOffset(rootElement, range);
|
const caret = rangeToTextOffset(rootElement, range);
|
||||||
|
|
||||||
if (key === 'ArrowLeft') {
|
if (key === 'ArrowLeft') {
|
||||||
if (caret !== 0) return false;
|
if (caret !== 0) return false;
|
||||||
} else {
|
} else {
|
||||||
const firstLineEnd = (first.textContent ?? '').indexOf('\n');
|
const firstLineEnd = (first.textContent ?? '').indexOf('\n');
|
||||||
|
|
||||||
if (firstLineEnd !== -1 && caret > firstLineEnd) return false;
|
if (firstLineEnd !== -1 && caret > firstLineEnd) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
|
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
|
||||||
rootElement.prepend(document.createElement('br'));
|
rootElement.prepend(document.createElement('br'));
|
||||||
restoreCaret(0, extend);
|
restoreCaret(0, extend);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,14 +501,17 @@
|
|||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
const first = rootElement.firstChild;
|
const first = rootElement.firstChild;
|
||||||
|
|
||||||
if (first?.nodeName !== 'BR') return;
|
if (first?.nodeName !== 'BR') return;
|
||||||
|
|
||||||
const second = first.nextSibling;
|
const second = first.nextSibling;
|
||||||
|
|
||||||
if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return;
|
if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return;
|
||||||
|
|
||||||
const range = safeRange();
|
const range = safeRange();
|
||||||
const onHatch =
|
const onHatch =
|
||||||
range !== null && range.startContainer === rootElement && range.startOffset === 0;
|
range !== null && range.startContainer === rootElement && range.startOffset === 0;
|
||||||
|
|
||||||
if (!onHatch) {
|
if (!onHatch) {
|
||||||
first.remove();
|
first.remove();
|
||||||
}
|
}
|
||||||
@@ -491,6 +531,7 @@
|
|||||||
*/
|
*/
|
||||||
function handleKeydown(event: KeyboardEvent) {
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
const mod = event.ctrlKey || event.metaKey;
|
const mod = event.ctrlKey || event.metaKey;
|
||||||
|
|
||||||
if (mod && !event.altKey && !isComposing && rootElement) {
|
if (mod && !event.altKey && !isComposing && rootElement) {
|
||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
const isUndo = key === 'z' && !event.shiftKey;
|
const isUndo = key === 'z' && !event.shiftKey;
|
||||||
@@ -499,11 +540,13 @@
|
|||||||
if (isUndo || isRedo) {
|
if (isUndo || isRedo) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const current = {
|
const current = {
|
||||||
value: lastEmittedValue,
|
caret: rangeToTextOffset(rootElement, safeRange()),
|
||||||
caret: rangeToTextOffset(rootElement, safeRange())
|
value: lastEmittedValue
|
||||||
};
|
};
|
||||||
const entry = isUndo ? history.undo(current) : history.redo(current);
|
const entry = isUndo ? history.undo(current) : history.redo(current);
|
||||||
|
|
||||||
if (entry) applyHistoryEntry(entry);
|
if (entry) applyHistoryEntry(entry);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,6 +567,7 @@
|
|||||||
// stuck on the old line (see insertLineBreak).
|
// stuck on the old line (see insertLineBreak).
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
insertLineBreak();
|
insertLineBreak();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,6 +587,7 @@
|
|||||||
// re-tokenize/re-highlight follows.
|
// re-tokenize/re-highlight follows.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.execCommand('insertLineBreak');
|
document.execCommand('insertLineBreak');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,6 +600,7 @@
|
|||||||
) {
|
) {
|
||||||
if (moveCaretBeforeLeadingCodeBlock(event.key, event.shiftKey)) {
|
if (moveCaretBeforeLeadingCodeBlock(event.key, event.shiftKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,6 +620,7 @@
|
|||||||
if (target !== null) {
|
if (target !== null) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
restoreCaret(target, event.shiftKey);
|
restoreCaret(target, event.shiftKey);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -586,6 +633,7 @@
|
|||||||
// change as our own and does not re-render.
|
// change as our own and does not re-render.
|
||||||
function applyHistoryEntry(entry: SourceHistoryEntry) {
|
function applyHistoryEntry(entry: SourceHistoryEntry) {
|
||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
renderTokens(tokenizeContent(entry.value));
|
renderTokens(tokenizeContent(entry.value));
|
||||||
lastEmittedValue = entry.value;
|
lastEmittedValue = entry.value;
|
||||||
value = entry.value;
|
value = entry.value;
|
||||||
@@ -602,6 +650,7 @@
|
|||||||
*/
|
*/
|
||||||
function handlePasteEvent(event: ClipboardEvent) {
|
function handlePasteEvent(event: ClipboardEvent) {
|
||||||
const pasted = event.clipboardData?.getData('text/plain');
|
const pasted = event.clipboardData?.getData('text/plain');
|
||||||
|
|
||||||
if (pasted && pasted.length > 0) {
|
if (pasted && pasted.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -609,6 +658,7 @@
|
|||||||
// element-boundary carets (e.g. right before a badge) Chromium's
|
// element-boundary carets (e.g. right before a badge) Chromium's
|
||||||
// insertText can drop the preceding text node's trailing whitespace.
|
// insertText can drop the preceding text node's trailing whitespace.
|
||||||
const range = safeRange();
|
const range = safeRange();
|
||||||
|
|
||||||
if (rootElement && range && range.collapsed) {
|
if (rootElement && range && range.collapsed) {
|
||||||
restoreCaret(rangeToTextOffset(rootElement, range));
|
restoreCaret(rangeToTextOffset(rootElement, range));
|
||||||
}
|
}
|
||||||
@@ -621,6 +671,7 @@
|
|||||||
// consumes the event (files, quoted prompts, long text).
|
// consumes the event (files, quoted prompts, long text).
|
||||||
function handlePaste(event: ClipboardEvent) {
|
function handlePaste(event: ClipboardEvent) {
|
||||||
onPaste?.(event);
|
onPaste?.(event);
|
||||||
|
|
||||||
if (!event.defaultPrevented) {
|
if (!event.defaultPrevented) {
|
||||||
handlePasteEvent(event);
|
handlePasteEvent(event);
|
||||||
}
|
}
|
||||||
@@ -634,20 +685,23 @@
|
|||||||
if (!rootElement) return null;
|
if (!rootElement) return null;
|
||||||
|
|
||||||
const range = safeRange();
|
const range = safeRange();
|
||||||
|
|
||||||
if (!range || range.collapsed) return null;
|
if (!range || range.collapsed) return null;
|
||||||
|
|
||||||
const startRange = range.cloneRange();
|
const startRange = range.cloneRange();
|
||||||
|
|
||||||
startRange.collapse(true);
|
startRange.collapse(true);
|
||||||
|
|
||||||
const source = serializeContent(rootElement);
|
const source = serializeContent(rootElement);
|
||||||
const start = rangeToTextOffset(rootElement, startRange);
|
const start = rangeToTextOffset(rootElement, startRange);
|
||||||
const end = rangeToTextOffset(rootElement, range);
|
const end = rangeToTextOffset(rootElement, range);
|
||||||
|
|
||||||
return { text: source.slice(start, end), range };
|
return { range, text: source.slice(start, end) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopy(event: ClipboardEvent) {
|
function handleCopy(event: ClipboardEvent) {
|
||||||
const slice = selectionSourceSlice();
|
const slice = selectionSourceSlice();
|
||||||
|
|
||||||
if (!slice) return;
|
if (!slice) return;
|
||||||
|
|
||||||
event.clipboardData?.setData('text/plain', slice.text);
|
event.clipboardData?.setData('text/plain', slice.text);
|
||||||
@@ -656,6 +710,7 @@
|
|||||||
|
|
||||||
function handleCut(event: ClipboardEvent) {
|
function handleCut(event: ClipboardEvent) {
|
||||||
const slice = selectionSourceSlice();
|
const slice = selectionSourceSlice();
|
||||||
|
|
||||||
if (!slice) return;
|
if (!slice) return;
|
||||||
|
|
||||||
event.clipboardData?.setData('text/plain', slice.text);
|
event.clipboardData?.setData('text/plain', slice.text);
|
||||||
@@ -675,6 +730,7 @@
|
|||||||
resizeHeight();
|
resizeHeight();
|
||||||
syncEmptyState();
|
syncEmptyState();
|
||||||
document.addEventListener('selectionchange', handleSelectionChange);
|
document.addEventListener('selectionchange', handleSelectionChange);
|
||||||
|
|
||||||
if (!isMobile.current) {
|
if (!isMobile.current) {
|
||||||
rootElement?.focus({ preventScroll: true });
|
rootElement?.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
@@ -689,6 +745,7 @@
|
|||||||
// browser already owns the right shape.
|
// browser already owns the right shape.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const incoming = value ?? '';
|
const incoming = value ?? '';
|
||||||
|
|
||||||
if (incoming === lastEmittedValue) return;
|
if (incoming === lastEmittedValue) return;
|
||||||
|
|
||||||
recordHistory(true); // external edit (mention insert, clear, ...): own undo step
|
recordHistory(true); // external edit (mention insert, clear, ...): own undo step
|
||||||
@@ -702,6 +759,7 @@
|
|||||||
|
|
||||||
export function getCaretOffset(): number {
|
export function getCaretOffset(): number {
|
||||||
if (!rootElement) return 0;
|
if (!rootElement) return 0;
|
||||||
|
|
||||||
return rangeToTextOffset(rootElement, safeRange());
|
return rangeToTextOffset(rootElement, safeRange());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,11 +768,13 @@
|
|||||||
if (rootElement && rootElement !== document.activeElement) {
|
if (rootElement && rootElement !== document.activeElement) {
|
||||||
rootElement.focus({ preventScroll: true });
|
rootElement.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreCaret(offset);
|
restoreCaret(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focus() {
|
export function focus() {
|
||||||
if (isMobile.current) return;
|
if (isMobile.current) return;
|
||||||
|
|
||||||
rootElement?.focus({ preventScroll: true });
|
rootElement?.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -1,9 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from 'svelte';
|
|
||||||
import { activeConversation, activeMessages } from '$lib/stores/conversations.svelte';
|
|
||||||
import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
|
|
||||||
import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
|
|
||||||
import ContextGaugeDial from './ContextGaugeDial.svelte';
|
import ContextGaugeDial from './ContextGaugeDial.svelte';
|
||||||
|
import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
|
||||||
|
import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
|
||||||
import {
|
import {
|
||||||
gaugeTriggerClick,
|
gaugeTriggerClick,
|
||||||
gaugeTriggerEnter,
|
gaugeTriggerEnter,
|
||||||
@@ -11,22 +9,28 @@
|
|||||||
gaugeTriggerLeave,
|
gaugeTriggerLeave,
|
||||||
gaugeTriggerPointerDown
|
gaugeTriggerPointerDown
|
||||||
} from '$lib/stores/context-gauge-popup.svelte';
|
} from '$lib/stores/context-gauge-popup.svelte';
|
||||||
|
import { activeConversation, activeMessages } from '$lib/stores/conversations.svelte';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
const gauge = useContextGauge();
|
const gauge = useContextGauge();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const conv = activeConversation();
|
const conv = activeConversation();
|
||||||
|
|
||||||
untrack(() => chatStore.setActiveProcessingConversation(conv?.id ?? null));
|
untrack(() => chatStore.setActiveProcessingConversation(conv?.id ?? null));
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const conv = activeConversation();
|
const conv = activeConversation();
|
||||||
const messages = activeMessages() as DatabaseMessage[];
|
const messages = activeMessages() as DatabaseMessage[];
|
||||||
|
|
||||||
if (!conv) return;
|
if (!conv) return;
|
||||||
|
|
||||||
if (isLoading() || isChatStreaming()) return;
|
if (isLoading() || isChatStreaming()) return;
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
untrack(() => chatStore.clearProcessingState(conv.id));
|
untrack(() => chatStore.clearProcessingState(conv.id));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { label, value, subtitle }: Props = $props();
|
let { label, subtitle, value }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid gap-1.5">
|
<div class="grid gap-1.5">
|
||||||
|
|||||||
+9
-9
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ContextGaugeDetailRow from './ContextGaugeDetailRow.svelte';
|
||||||
import { ChevronDown } from '@lucide/svelte';
|
import { ChevronDown } from '@lucide/svelte';
|
||||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
import { STATS_UNITS } from '$lib/constants';
|
import { STATS_UNITS } from '$lib/constants';
|
||||||
import ContextGaugeDetailRow from './ContextGaugeDetailRow.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentRead: number;
|
currentRead: number;
|
||||||
@@ -18,15 +18,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
currentRead,
|
|
||||||
currentFresh,
|
|
||||||
currentCache,
|
|
||||||
currentOutput,
|
|
||||||
kvTotal,
|
|
||||||
cumulativeRead,
|
|
||||||
cumulativeOutput,
|
|
||||||
cumulativeCacheTotal,
|
|
||||||
averageTokensPerSecond,
|
averageTokensPerSecond,
|
||||||
|
cumulativeCacheTotal,
|
||||||
|
cumulativeOutput,
|
||||||
|
cumulativeRead,
|
||||||
|
currentCache,
|
||||||
|
currentFresh,
|
||||||
|
currentOutput,
|
||||||
|
currentRead,
|
||||||
|
kvTotal,
|
||||||
transientDetails
|
transientDetails
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
}
|
}
|
||||||
|
|
||||||
let { percent, level, size = 'sm' }: Props = $props();
|
let { level, percent, size = 'sm' }: Props = $props();
|
||||||
|
|
||||||
const RADIUS = 11;
|
const RADIUS = 11;
|
||||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
onLoad: () => void;
|
onLoad: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { modelId, isLoading, onLoad }: Props = $props();
|
let { isLoading, modelId, onLoad }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if modelId !== null && !isLoading}
|
{#if modelId !== null && !isLoading}
|
||||||
|
|||||||
+9
-4
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatParameters } from '$lib/utils/formatters';
|
import { colorLevelBgClass, colorLevelTextClass } from './context-gauge';
|
||||||
import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
|
|
||||||
import ContextGaugeDetails from './ContextGaugeDetails.svelte';
|
import ContextGaugeDetails from './ContextGaugeDetails.svelte';
|
||||||
import ContextGaugeLoadModel from './ContextGaugeLoadModel.svelte';
|
import ContextGaugeLoadModel from './ContextGaugeLoadModel.svelte';
|
||||||
import { colorLevelBgClass, colorLevelTextClass } from './context-gauge';
|
import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
|
||||||
import {
|
import {
|
||||||
gaugePopup,
|
|
||||||
gaugeCardEnter,
|
gaugeCardEnter,
|
||||||
gaugeCardLeave,
|
gaugeCardLeave,
|
||||||
|
gaugePopup,
|
||||||
gaugePopupClose
|
gaugePopupClose
|
||||||
} from '$lib/stores/context-gauge-popup.svelte';
|
} from '$lib/stores/context-gauge-popup.svelte';
|
||||||
|
import { formatParameters } from '$lib/utils/formatters';
|
||||||
|
|
||||||
const gauge = useContextGauge();
|
const gauge = useContextGauge();
|
||||||
|
|
||||||
@@ -30,13 +30,18 @@
|
|||||||
|
|
||||||
const onPointerDown = (event: PointerEvent) => {
|
const onPointerDown = (event: PointerEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
|
|
||||||
if (!(target instanceof Node)) return;
|
if (!(target instanceof Node)) return;
|
||||||
|
|
||||||
if (cardEl?.contains(target)) return;
|
if (cardEl?.contains(target)) return;
|
||||||
|
|
||||||
if (target instanceof Element && target.closest('[data-context-gauge-trigger]')) return;
|
if (target instanceof Element && target.closest('[data-context-gauge-trigger]')) return;
|
||||||
|
|
||||||
gaugePopupClose();
|
gaugePopupClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('pointerdown', onPointerDown, true);
|
document.addEventListener('pointerdown', onPointerDown, true);
|
||||||
|
|
||||||
return () => document.removeEventListener('pointerdown', onPointerDown, true);
|
return () => document.removeEventListener('pointerdown', onPointerDown, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ const CRITICAL_THRESHOLD = 95;
|
|||||||
|
|
||||||
export function colorLevelFromPercent(percent: number | null): ColorLevel {
|
export function colorLevelFromPercent(percent: number | null): ColorLevel {
|
||||||
if (percent === null) return 'neutral';
|
if (percent === null) return 'neutral';
|
||||||
|
|
||||||
if (percent >= CRITICAL_THRESHOLD) return 'critical';
|
if (percent >= CRITICAL_THRESHOLD) return 'critical';
|
||||||
|
|
||||||
if (percent >= WARNING_THRESHOLD) return 'warning';
|
if (percent >= WARNING_THRESHOLD) return 'warning';
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
|
||||||
import {
|
|
||||||
mcpResourceAttachments,
|
|
||||||
mcpHasResourceAttachments
|
|
||||||
} from '$lib/stores/mcp-resources.svelte';
|
|
||||||
import {
|
import {
|
||||||
ChatAttachmentsListItemMcpResource,
|
ChatAttachmentsListItemMcpResource,
|
||||||
HorizontalScrollCarousel
|
HorizontalScrollCarousel
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import {
|
||||||
|
mcpHasResourceAttachments,
|
||||||
|
mcpResourceAttachments
|
||||||
|
} from '$lib/stores/mcp-resources.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
+17
-10
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FolderOpen, Sparkles } from '@lucide/svelte';
|
import { FolderOpen, Sparkles } from '@lucide/svelte';
|
||||||
import { MODEL_SELECTOR_ICON } from '$lib/constants';
|
|
||||||
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
|
||||||
import { ChatFormCommandAction } from '$lib/enums';
|
|
||||||
import type { ChatFormCommand } from '$lib/types';
|
|
||||||
import {
|
import {
|
||||||
ChatFormPickerList,
|
ChatFormPickerList,
|
||||||
ChatFormPickerListItem,
|
ChatFormPickerListItem,
|
||||||
ChatFormPickerPopover
|
ChatFormPickerPopover
|
||||||
} from '$lib/components/app/chat';
|
} from '$lib/components/app/chat';
|
||||||
|
import { MODEL_SELECTOR_ICON } from '$lib/constants';
|
||||||
|
import { ChatFormCommandAction } from '$lib/enums';
|
||||||
|
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
||||||
|
import type { ChatFormCommand } from '$lib/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slash-command picker; `query` (typed after `/`) filters the commands.
|
* Slash-command picker; `query` (typed after `/`) filters the commands.
|
||||||
@@ -24,12 +24,12 @@
|
|||||||
onSelect: (command: ChatFormCommand) => void;
|
onSelect: (command: ChatFormCommand) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { class: className = '', isOpen, query, commands, onClose, onSelect }: Props = $props();
|
let { class: className = '', commands, isOpen, onClose, onSelect, query }: Props = $props();
|
||||||
|
|
||||||
const commandIcon: Record<ChatFormCommandAction, typeof Sparkles> = {
|
const commandIcon: Record<ChatFormCommandAction, typeof Sparkles> = {
|
||||||
[ChatFormCommandAction.PROMPT]: Sparkles,
|
|
||||||
[ChatFormCommandAction.CWD]: FolderOpen,
|
[ChatFormCommandAction.CWD]: FolderOpen,
|
||||||
[ChatFormCommandAction.MODEL]: MODEL_SELECTOR_ICON
|
[ChatFormCommandAction.MODEL]: MODEL_SELECTOR_ICON,
|
||||||
|
[ChatFormCommandAction.PROMPT]: Sparkles
|
||||||
};
|
};
|
||||||
|
|
||||||
const trimmedQuery = $derived((query ?? '').trim().toLowerCase());
|
const trimmedQuery = $derived((query ?? '').trim().toLowerCase());
|
||||||
@@ -51,20 +51,24 @@
|
|||||||
|
|
||||||
function stepEnabled(from: number, dir: number): number {
|
function stepEnabled(from: number, dir: number): number {
|
||||||
const n = filteredCommands.length;
|
const n = filteredCommands.length;
|
||||||
|
|
||||||
if (n === 0) return -1;
|
if (n === 0) return -1;
|
||||||
|
|
||||||
for (let i = 1; i <= n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
const idx = (from + dir * i + n) % n;
|
const idx = (from + dir * i + n) % n;
|
||||||
|
|
||||||
if (!filteredCommands[idx].disabled) return idx;
|
if (!filteredCommands[idx].disabled) return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nav = usePickerNavigation({
|
const nav = usePickerNavigation({
|
||||||
isOpen: () => isOpen,
|
|
||||||
count: () => filteredCommands.length,
|
count: () => filteredCommands.length,
|
||||||
step: (from, dir) => (from < 0 ? firstEnabledIndex() : stepEnabled(from, dir)),
|
isOpen: () => isOpen,
|
||||||
onClose: () => onClose(),
|
onClose: () => onClose(),
|
||||||
onSelect: (index) => handleSelect(filteredCommands[index])
|
onSelect: (index) => handleSelect(filteredCommands[index]),
|
||||||
|
step: (from, dir) => (from < 0 ? firstEnabledIndex() : stepEnabled(from, dir))
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -76,8 +80,10 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (nav.hoveredIndex < 0 || nav.hoveredIndex >= filteredCommands.length) {
|
if (nav.hoveredIndex < 0 || nav.hoveredIndex >= filteredCommands.length) {
|
||||||
nav.reset(firstEnabledIndex());
|
nav.reset(firstEnabledIndex());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredCommands[nav.hoveredIndex].disabled) {
|
if (filteredCommands[nav.hoveredIndex].disabled) {
|
||||||
nav.reset(firstEnabledIndex());
|
nav.reset(firstEnabledIndex());
|
||||||
}
|
}
|
||||||
@@ -85,6 +91,7 @@
|
|||||||
|
|
||||||
function handleSelect(command: ChatFormCommand) {
|
function handleSelect(command: ChatFormCommand) {
|
||||||
if (command.disabled) return;
|
if (command.disabled) return;
|
||||||
|
|
||||||
onSelect(command);
|
onSelect(command);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-18
@@ -1,22 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { File, Folder } from '@lucide/svelte';
|
import { File, Folder } from '@lucide/svelte';
|
||||||
import { abbreviateHome, runGlobSearchWithChildren, type GlobEntryResult } from '$lib/utils';
|
import { ChatFormPickerList, ChatFormPickerListItem } from '$lib/components/app/chat';
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
import HighlightedMatch from '$lib/components/app/forms/HighlightedMatch.svelte';
|
||||||
import { BuiltInTool, FileMentionEntryType, GlobSearchType, KeyboardKey } from '$lib/enums';
|
|
||||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import HighlightedMatch from '$lib/components/app/forms/HighlightedMatch.svelte';
|
|
||||||
import { ChatFormPickerList, ChatFormPickerListItem } from '$lib/components/app/chat';
|
|
||||||
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
|
|
||||||
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
|
||||||
import type { FileMentionEntry } from '$lib/types';
|
|
||||||
import {
|
import {
|
||||||
FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
|
FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
|
||||||
HOME_TILDE,
|
HOME_TILDE,
|
||||||
SEARCH_DEBOUNCE_MS
|
SEARCH_DEBOUNCE_MS
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { BuiltInTool, FileMentionEntryType, GlobSearchType, KeyboardKey } from '$lib/enums';
|
||||||
|
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
|
||||||
|
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
||||||
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||||
|
import type { FileMentionEntry } from '$lib/types';
|
||||||
|
import { abbreviateHome, type GlobEntryResult, runGlobSearchWithChildren } from '$lib/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Floating file/folder mention picker. The chat input is the search
|
* Floating file/folder mention picker. The chat input is the search
|
||||||
@@ -38,18 +38,18 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
isOpen,
|
|
||||||
query,
|
|
||||||
customAnchor = null,
|
customAnchor = null,
|
||||||
scopePath = null,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
onOpened,
|
||||||
onSelect,
|
onSelect,
|
||||||
onOpened
|
query,
|
||||||
|
scopePath = null
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const nav = usePickerNavigation({
|
const nav = usePickerNavigation({
|
||||||
isOpen: () => isOpen,
|
|
||||||
count: () => displayedItems.length,
|
count: () => displayedItems.length,
|
||||||
|
isOpen: () => isOpen,
|
||||||
onClose: () => onClose(),
|
onClose: () => onClose(),
|
||||||
onSelect: (index) => handleSelect(displayedItems[index])
|
onSelect: (index) => handleSelect(displayedItems[index])
|
||||||
});
|
});
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
// would otherwise reach the server as max_depth 0 = unlimited.
|
// would otherwise reach the server as max_depth 0 = unlimited.
|
||||||
const searchDepth = $derived.by(() => {
|
const searchDepth = $derived.by(() => {
|
||||||
const n = Number(config().mentionSearchMaxDepth);
|
const n = Number(config().mentionSearchMaxDepth);
|
||||||
|
|
||||||
return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH;
|
return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,8 +79,8 @@
|
|||||||
const MENTION_SEARCH_LIMIT = 50;
|
const MENTION_SEARCH_LIMIT = 50;
|
||||||
|
|
||||||
const search = useDebouncedSearch({
|
const search = useDebouncedSearch({
|
||||||
debounceMs: SEARCH_DEBOUNCE_MS,
|
|
||||||
canRun: () => isOpen && fileSearchEnabled,
|
canRun: () => isOpen && fileSearchEnabled,
|
||||||
|
debounceMs: SEARCH_DEBOUNCE_MS,
|
||||||
getQuery: () => trimmedQuery,
|
getQuery: () => trimmedQuery,
|
||||||
run: async (query, signal, isCurrent) => {
|
run: async (query, signal, isCurrent) => {
|
||||||
try {
|
try {
|
||||||
@@ -91,23 +92,29 @@
|
|||||||
searchDepth,
|
searchDepth,
|
||||||
MENTION_SEARCH_LIMIT,
|
MENTION_SEARCH_LIMIT,
|
||||||
signal,
|
signal,
|
||||||
{ type: GlobSearchType.ALL, descendOnTrailingSeparator: true }
|
{ descendOnTrailingSeparator: true, type: GlobSearchType.ALL }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
searchResults = [];
|
searchResults = [];
|
||||||
searchError = res.error;
|
searchError = res.error;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toEntry = (e: GlobEntryResult): FileMentionEntry => ({
|
const toEntry = (e: GlobEntryResult): FileMentionEntry => ({
|
||||||
path: e.path,
|
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
path: e.path,
|
||||||
type: e.type === 'dir' ? FileMentionEntryType.DIRECTORY : FileMentionEntryType.FILE
|
type: e.type === 'dir' ? FileMentionEntryType.DIRECTORY : FileMentionEntryType.FILE
|
||||||
});
|
});
|
||||||
|
|
||||||
searchResults = res.entries.map(toEntry);
|
searchResults = res.entries.map(toEntry);
|
||||||
searchError = null;
|
searchError = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isCurrent() || signal.aborted) return;
|
if (!isCurrent() || signal.aborted) return;
|
||||||
|
|
||||||
searchResults = [];
|
searchResults = [];
|
||||||
searchError = err instanceof Error ? err.message : String(err);
|
searchError = err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
@@ -121,9 +128,11 @@
|
|||||||
if (fileSearchKey === null) {
|
if (fileSearchKey === null) {
|
||||||
return 'File search is unavailable on this server (started without --tools)';
|
return 'File search is unavailable on this server (started without --tools)';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fileSearchEnabled) {
|
if (!fileSearchEnabled) {
|
||||||
return 'File search is disabled - enable "Search files" in Settings > Tools to use @-mentions';
|
return 'File search is disabled - enable "Search files" in Settings > Tools to use @-mentions';
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchError ? `Search failed - ${searchError}` : 'No matching files or folders';
|
return searchError ? `Search failed - ${searchError}` : 'No matching files or folders';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,6 +140,7 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
void toolsStore.resolveServerHome();
|
void toolsStore.resolveServerHome();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,12 +156,15 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const q = (query ?? '').trim();
|
const q = (query ?? '').trim();
|
||||||
|
|
||||||
if (!isOpen || !q || !fileSearchEnabled) {
|
if (!isOpen || !q || !fileSearchEnabled) {
|
||||||
search.cancel();
|
search.cancel();
|
||||||
searchResults = [];
|
searchResults = [];
|
||||||
searchError = null;
|
searchError = null;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
search.setLoading(true);
|
search.setLoading(true);
|
||||||
search.run(q);
|
search.run(q);
|
||||||
});
|
});
|
||||||
@@ -167,9 +180,11 @@
|
|||||||
// Enter-to-submit never fires mid-search.
|
// Enter-to-submit never fires mid-search.
|
||||||
if (isOpen && event.key === KeyboardKey.ENTER) {
|
if (isOpen && event.key === KeyboardKey.ENTER) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (nav.hoveredIndex >= 0 && displayedItems[nav.hoveredIndex]) {
|
if (nav.hoveredIndex >= 0 && displayedItems[nav.hoveredIndex]) {
|
||||||
handleSelect(displayedItems[nav.hoveredIndex]);
|
handleSelect(displayedItems[nav.hoveredIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
server: MCPServerSettingsEntry | undefined;
|
server: MCPServerSettingsEntry | undefined;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
subtitle?: Snippet;
|
subtitle?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { server, serverLabel, title, description, titleExtra, subtitle }: Props = $props();
|
let { description, server, serverLabel, subtitle, title, titleExtra }: Props = $props();
|
||||||
|
|
||||||
let faviconUrl = $derived(server ? mcpStore.getServerFavicon(server.id) : null);
|
let faviconUrl = $derived(server ? mcpStore.getServerFavicon(server.id) : null);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+19
-19
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" generics="T">
|
<script lang="ts" generics="T">
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import { SearchInput } from '$lib/components/app';
|
import { SearchInput } from '$lib/components/app';
|
||||||
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
|
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
|
||||||
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
|
|
||||||
import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants';
|
import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants';
|
||||||
|
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: T[];
|
items: T[];
|
||||||
@@ -28,22 +28,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
items,
|
|
||||||
isLoading,
|
|
||||||
selectedIndex,
|
|
||||||
searchQuery = $bindable(),
|
|
||||||
showSearchInput,
|
|
||||||
searchPlaceholder = 'Search...',
|
|
||||||
emptyMessage,
|
|
||||||
autofocus = false,
|
autofocus = false,
|
||||||
inputRef = $bindable(null),
|
emptyMessage,
|
||||||
onSearchClose,
|
|
||||||
itemKey,
|
|
||||||
item,
|
|
||||||
skeleton,
|
|
||||||
skeletonCount = 6,
|
|
||||||
footer,
|
footer,
|
||||||
scrollTrigger
|
inputRef = $bindable(null),
|
||||||
|
isLoading,
|
||||||
|
item,
|
||||||
|
itemKey,
|
||||||
|
items,
|
||||||
|
onSearchClose,
|
||||||
|
scrollTrigger,
|
||||||
|
searchPlaceholder = 'Search...',
|
||||||
|
searchQuery = $bindable(),
|
||||||
|
selectedIndex,
|
||||||
|
showSearchInput,
|
||||||
|
skeleton,
|
||||||
|
skeletonCount = 6
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let listContainer = $state<HTMLDivElement | null>(null);
|
let listContainer = $state<HTMLDivElement | null>(null);
|
||||||
@@ -55,11 +55,11 @@
|
|||||||
// selectedIndex/items.length are untracked so hover and result replacement
|
// selectedIndex/items.length are untracked so hover and result replacement
|
||||||
// never re-fire the scroll; keyboard nav is the only path that bumps the trigger.
|
// never re-fire the scroll; keyboard nav is the only path that bumps the trigger.
|
||||||
useScrollActiveRow({
|
useScrollActiveRow({
|
||||||
getTrigger: () => scrollTrigger,
|
dataIndex: 'picker',
|
||||||
getContainer: () => listContainer,
|
getContainer: () => listContainer,
|
||||||
getIndex: () => selectedIndex,
|
|
||||||
getCount: () => items.length,
|
getCount: () => items.length,
|
||||||
dataIndex: 'picker'
|
getIndex: () => selectedIndex,
|
||||||
|
getTrigger: () => scrollTrigger
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -12,13 +12,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
children,
|
||||||
class: className = '',
|
class: className = '',
|
||||||
isSelected = false,
|
|
||||||
disabled = false,
|
|
||||||
onclick,
|
|
||||||
onmouseenter,
|
|
||||||
dataIndex,
|
dataIndex,
|
||||||
children
|
disabled = false,
|
||||||
|
isSelected = false,
|
||||||
|
onclick,
|
||||||
|
onmouseenter
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
showBadge?: boolean;
|
showBadge?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { titleWidth = 'w-48', showBadge = false }: Props = $props();
|
let { showBadge = false, titleWidth = 'w-48' }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full items-start gap-3 rounded-lg px-3 py-2">
|
<div class="flex w-full items-start gap-3 rounded-lg px-3 py-2">
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
children,
|
||||||
class: className = '',
|
class: className = '',
|
||||||
isOpen = $bindable(false),
|
isOpen = $bindable(false),
|
||||||
srLabel = 'Open picker',
|
|
||||||
onClose,
|
onClose,
|
||||||
onKeydown,
|
onKeydown,
|
||||||
children
|
srLabel = 'Open picker'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+26
-19
@@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
|
||||||
import { debounce, uuid } from '$lib/utils';
|
|
||||||
import { KeyboardKey } from '$lib/enums';
|
|
||||||
import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
|
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
|
||||||
import {
|
import {
|
||||||
ChatFormPickerPopover,
|
ChatFormPickerItemHeader,
|
||||||
ChatFormPickerList,
|
ChatFormPickerList,
|
||||||
ChatFormPickerListItem,
|
ChatFormPickerListItem,
|
||||||
ChatFormPickerItemHeader,
|
|
||||||
ChatFormPickerListItemSkeleton,
|
ChatFormPickerListItemSkeleton,
|
||||||
|
ChatFormPickerPopover,
|
||||||
ChatFormPromptPickerArgumentForm
|
ChatFormPromptPickerArgumentForm
|
||||||
} from '$lib/components/app/chat';
|
} from '$lib/components/app/chat';
|
||||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||||
|
import { KeyboardKey } from '$lib/enums';
|
||||||
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import type { GetPromptResult, MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
|
||||||
|
import { debounce, uuid } from '$lib/utils';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
isOpen = false,
|
isOpen = false,
|
||||||
searchQuery = '',
|
|
||||||
onClose,
|
onClose,
|
||||||
onPromptLoadStart,
|
|
||||||
onPromptLoadComplete,
|
onPromptLoadComplete,
|
||||||
onPromptLoadError
|
onPromptLoadError,
|
||||||
|
onPromptLoadStart,
|
||||||
|
searchQuery = ''
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let prompts = $state<MCPPromptInfo[]>([]);
|
let prompts = $state<MCPPromptInfo[]>([]);
|
||||||
@@ -89,7 +89,6 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
|
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
|
||||||
|
|
||||||
const initialized = await mcpStore.ensureInitialized(perChatOverrides);
|
const initialized = await mcpStore.ensureInitialized(perChatOverrides);
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
@@ -118,6 +117,7 @@
|
|||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const firstInput = document.querySelector(`#arg-${args[0].name}`) as HTMLInputElement;
|
const firstInput = document.querySelector(`#arg-${args[0].name}`) as HTMLInputElement;
|
||||||
|
|
||||||
if (firstInput) {
|
if (firstInput) {
|
||||||
firstInput.focus();
|
firstInput.focus();
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,6 @@
|
|||||||
promptError = null;
|
promptError = null;
|
||||||
|
|
||||||
const placeholderId = uuid();
|
const placeholderId = uuid();
|
||||||
|
|
||||||
const nonEmptyArgs = Object.fromEntries(
|
const nonEmptyArgs = Object.fromEntries(
|
||||||
Object.entries(args).filter(([, value]) => value.trim() !== '')
|
Object.entries(args).filter(([, value]) => value.trim() !== '')
|
||||||
);
|
);
|
||||||
@@ -142,10 +141,12 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await mcpStore.getPrompt(prompt.serverName, prompt.name, args);
|
const result = await mcpStore.getPrompt(prompt.serverName, prompt.name, args);
|
||||||
|
|
||||||
onPromptLoadComplete?.(placeholderId, result);
|
onPromptLoadComplete?.(placeholderId, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : 'Unknown error executing prompt';
|
error instanceof Error ? error.message : 'Unknown error executing prompt';
|
||||||
|
|
||||||
onPromptLoadError?.(placeholderId, errorMessage);
|
onPromptLoadError?.(placeholderId, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,9 +168,9 @@
|
|||||||
|
|
||||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||||
console.log('[ChatFormPickerMcpPrompts] Fetching completions for:', {
|
console.log('[ChatFormPickerMcpPrompts] Fetching completions for:', {
|
||||||
serverName: selectedPrompt.serverName,
|
|
||||||
promptName: selectedPrompt.name,
|
|
||||||
argName,
|
argName,
|
||||||
|
promptName: selectedPrompt.name,
|
||||||
|
serverName: selectedPrompt.serverName,
|
||||||
value
|
value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -187,9 +188,9 @@
|
|||||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||||
console.log('[ChatFormPickerMcpPrompts] Autocomplete result:', {
|
console.log('[ChatFormPickerMcpPrompts] Autocomplete result:', {
|
||||||
argName,
|
argName,
|
||||||
value,
|
|
||||||
result,
|
result,
|
||||||
suggestionsCount: result?.values.length ?? 0
|
suggestionsCount: result?.values.length ?? 0,
|
||||||
|
value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +235,7 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleCancelArgumentForm();
|
handleCancelArgumentForm();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +276,7 @@
|
|||||||
selectedIndex = selectedIndexBeforeArgumentForm;
|
selectedIndex = selectedIndexBeforeArgumentForm;
|
||||||
selectedIndexBeforeArgumentForm = null;
|
selectedIndexBeforeArgumentForm = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedPrompt = null;
|
selectedPrompt = null;
|
||||||
promptArgs = {};
|
promptArgs = {};
|
||||||
promptError = null;
|
promptError = null;
|
||||||
@@ -284,6 +287,7 @@
|
|||||||
|
|
||||||
if (event.key === KeyboardKey.ESCAPE) {
|
if (event.key === KeyboardKey.ESCAPE) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (selectedPrompt) {
|
if (selectedPrompt) {
|
||||||
// Return to prompt selection list, keeping the selected prompt active
|
// Return to prompt selection list, keeping the selected prompt active
|
||||||
handleCancelArgumentForm();
|
handleCancelArgumentForm();
|
||||||
@@ -296,6 +300,7 @@
|
|||||||
|
|
||||||
if (event.key === KeyboardKey.ARROW_DOWN) {
|
if (event.key === KeyboardKey.ARROW_DOWN) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (filteredPrompts.length > 0) {
|
if (filteredPrompts.length > 0) {
|
||||||
selectedIndex = (selectedIndex + 1) % filteredPrompts.length;
|
selectedIndex = (selectedIndex + 1) % filteredPrompts.length;
|
||||||
scrollTrigger++;
|
scrollTrigger++;
|
||||||
@@ -306,6 +311,7 @@
|
|||||||
|
|
||||||
if (event.key === KeyboardKey.ARROW_UP) {
|
if (event.key === KeyboardKey.ARROW_UP) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (filteredPrompts.length > 0) {
|
if (filteredPrompts.length > 0) {
|
||||||
selectedIndex = selectedIndex === 0 ? filteredPrompts.length - 1 : selectedIndex - 1;
|
selectedIndex = selectedIndex === 0 ? filteredPrompts.length - 1 : selectedIndex - 1;
|
||||||
scrollTrigger++;
|
scrollTrigger++;
|
||||||
@@ -316,6 +322,7 @@
|
|||||||
|
|
||||||
if (event.key === KeyboardKey.ENTER && !selectedPrompt) {
|
if (event.key === KeyboardKey.ENTER && !selectedPrompt) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (filteredPrompts[selectedIndex]) {
|
if (filteredPrompts[selectedIndex]) {
|
||||||
handlePromptClick(filteredPrompts[selectedIndex]);
|
handlePromptClick(filteredPrompts[selectedIndex]);
|
||||||
}
|
}
|
||||||
@@ -329,14 +336,14 @@
|
|||||||
let filteredPrompts = $derived.by(() => {
|
let filteredPrompts = $derived.by(() => {
|
||||||
const sortedServers = mcpStore.getServers();
|
const sortedServers = mcpStore.getServers();
|
||||||
const serverOrderMap = new Map(sortedServers.map((server, index) => [server.id, index]));
|
const serverOrderMap = new Map(sortedServers.map((server, index) => [server.id, index]));
|
||||||
|
|
||||||
const sortedPrompts = [...prompts].sort((a, b) => {
|
const sortedPrompts = [...prompts].sort((a, b) => {
|
||||||
const orderA = serverOrderMap.get(a.serverName) ?? Number.MAX_SAFE_INTEGER;
|
const orderA = serverOrderMap.get(a.serverName) ?? Number.MAX_SAFE_INTEGER;
|
||||||
const orderB = serverOrderMap.get(b.serverName) ?? Number.MAX_SAFE_INTEGER;
|
const orderB = serverOrderMap.get(b.serverName) ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
return orderA - orderB;
|
return orderA - orderB;
|
||||||
});
|
});
|
||||||
|
|
||||||
const query = (searchQuery || internalSearchQuery).toLowerCase();
|
const query = (searchQuery || internalSearchQuery).toLowerCase();
|
||||||
|
|
||||||
if (!query) return sortedPrompts;
|
if (!query) return sortedPrompts;
|
||||||
|
|
||||||
return sortedPrompts.filter(
|
return sortedPrompts.filter(
|
||||||
|
|||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { MCPPromptInfo } from '$lib/types';
|
|
||||||
import ChatFormPromptPickerArgumentInput from './ChatFormPromptPickerArgumentInput.svelte';
|
import ChatFormPromptPickerArgumentInput from './ChatFormPromptPickerArgumentInput.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import type { MCPPromptInfo } from '$lib/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
prompt: MCPPromptInfo;
|
prompt: MCPPromptInfo;
|
||||||
@@ -21,20 +21,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
prompt,
|
|
||||||
promptArgs,
|
|
||||||
suggestions,
|
|
||||||
loadingSuggestions,
|
|
||||||
activeAutocomplete,
|
activeAutocomplete,
|
||||||
autocompleteIndex,
|
autocompleteIndex,
|
||||||
promptError,
|
loadingSuggestions,
|
||||||
onArgInput,
|
|
||||||
onArgKeydown,
|
|
||||||
onArgBlur,
|
onArgBlur,
|
||||||
onArgFocus,
|
onArgFocus,
|
||||||
|
onArgInput,
|
||||||
|
onArgKeydown,
|
||||||
|
onCancel,
|
||||||
onSelectSuggestion,
|
onSelectSuggestion,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel
|
prompt,
|
||||||
|
promptArgs,
|
||||||
|
promptError,
|
||||||
|
suggestions
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { MCPPromptInfo } from '$lib/types';
|
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import type { MCPPromptInfo } from '$lib/types';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
type PromptArgument = NonNullable<MCPPromptInfo['arguments']>[number];
|
type PromptArgument = NonNullable<MCPPromptInfo['arguments']>[number];
|
||||||
|
|
||||||
@@ -22,16 +22,16 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
argument,
|
argument,
|
||||||
value = '',
|
|
||||||
suggestions = [],
|
|
||||||
isLoadingSuggestions = false,
|
|
||||||
isAutocompleteActive = false,
|
|
||||||
autocompleteIndex = 0,
|
autocompleteIndex = 0,
|
||||||
onInput,
|
isAutocompleteActive = false,
|
||||||
onKeydown,
|
isLoadingSuggestions = false,
|
||||||
onBlur,
|
onBlur,
|
||||||
onFocus,
|
onFocus,
|
||||||
onSelectSuggestion
|
onInput,
|
||||||
|
onKeydown,
|
||||||
|
onSelectSuggestion,
|
||||||
|
suggestions = [],
|
||||||
|
value = ''
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{#if isAutocompleteActive && suggestions.length > 0}
|
{#if isAutocompleteActive && suggestions.length > 0}
|
||||||
<div
|
<div
|
||||||
class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
|
class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
|
||||||
transition:fly={{ y: -5, duration: 100 }}
|
transition:fly={{ duration: 100, y: -5 }}
|
||||||
>
|
>
|
||||||
{#each suggestions as suggestion, i (suggestion)}
|
{#each suggestions as suggestion, i (suggestion)}
|
||||||
<button
|
<button
|
||||||
|
|||||||
+11
-11
@@ -35,24 +35,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
isCommandPickerOpen,
|
|
||||||
commandQuery,
|
commandQuery,
|
||||||
commands = [],
|
commands = [],
|
||||||
|
isCommandPickerOpen,
|
||||||
|
isMentionPickerOpen,
|
||||||
|
isPromptPickerOpen,
|
||||||
|
mentionAnchor,
|
||||||
|
mentionQuery,
|
||||||
onCommandPickerClose,
|
onCommandPickerClose,
|
||||||
onCommandSelect,
|
onCommandSelect,
|
||||||
isPromptPickerOpen,
|
|
||||||
promptSearchQuery,
|
|
||||||
isMentionPickerOpen,
|
|
||||||
mentionQuery,
|
|
||||||
mentionAnchor,
|
|
||||||
scopePath,
|
|
||||||
onPromptPickerClose,
|
|
||||||
onMentionPickerClose,
|
|
||||||
onMentionOpened,
|
onMentionOpened,
|
||||||
|
onMentionPickerClose,
|
||||||
onMentionSelect,
|
onMentionSelect,
|
||||||
onPromptLoadStart,
|
|
||||||
onPromptLoadComplete,
|
onPromptLoadComplete,
|
||||||
onPromptLoadError
|
onPromptLoadError,
|
||||||
|
onPromptLoadStart,
|
||||||
|
onPromptPickerClose,
|
||||||
|
promptSearchQuery,
|
||||||
|
scopePath
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let commandPickerRef: ChatFormCommandPicker | undefined = $state(undefined);
|
let commandPickerRef: ChatFormCommandPicker | undefined = $state(undefined);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
// the picker/paste flows can address either renderer through one handle.
|
// the picker/paste flows can address either renderer through one handle.
|
||||||
export function getCaretOffset(): number {
|
export function getCaretOffset(): number {
|
||||||
if (!textareaElement) return 0;
|
if (!textareaElement) return 0;
|
||||||
|
|
||||||
return textareaElement.selectionStart ?? textareaElement.value.length;
|
return textareaElement.selectionStart ?? textareaElement.value.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FolderOpen } from '@lucide/svelte';
|
|
||||||
import { ToolsService } from '$lib/services/tools.service';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import { BuiltInTool, GlobSearchType, KeyboardKey } from '$lib/enums';
|
|
||||||
import {
|
|
||||||
abbreviateHome,
|
|
||||||
buildCaseInsensitiveGlob,
|
|
||||||
joinPath,
|
|
||||||
lastPathSegment,
|
|
||||||
runGlobSearchWithChildren,
|
|
||||||
type GlobEntry
|
|
||||||
} from '$lib/utils';
|
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
|
||||||
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
|
|
||||||
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
|
|
||||||
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
|
||||||
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
|
|
||||||
import ChatFormWorkingDirectoryChip from './ChatFormWorkingDirectoryChip.svelte';
|
import ChatFormWorkingDirectoryChip from './ChatFormWorkingDirectoryChip.svelte';
|
||||||
import ChatFormWorkingDirectoryResultsList from './ChatFormWorkingDirectoryResultsList.svelte';
|
import ChatFormWorkingDirectoryResultsList from './ChatFormWorkingDirectoryResultsList.svelte';
|
||||||
|
import { FolderOpen } from '@lucide/svelte';
|
||||||
|
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
|
||||||
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import {
|
import {
|
||||||
DEFAULT_MOBILE_BREAKPOINT,
|
DEFAULT_MOBILE_BREAKPOINT,
|
||||||
HOME_TILDE,
|
HOME_TILDE,
|
||||||
@@ -28,6 +14,20 @@
|
|||||||
SEARCH_LIMIT,
|
SEARCH_LIMIT,
|
||||||
SEARCH_MAX_DEPTH
|
SEARCH_MAX_DEPTH
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { BuiltInTool, GlobSearchType, KeyboardKey } from '$lib/enums';
|
||||||
|
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
|
||||||
|
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
||||||
|
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
|
||||||
|
import { ToolsService } from '$lib/services/tools.service';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import {
|
||||||
|
abbreviateHome,
|
||||||
|
buildCaseInsensitiveGlob,
|
||||||
|
type GlobEntry,
|
||||||
|
joinPath,
|
||||||
|
lastPathSegment,
|
||||||
|
runGlobSearchWithChildren
|
||||||
|
} from '$lib/utils';
|
||||||
|
|
||||||
// Microtask delay so the popover's focus scope tears down first.
|
// Microtask delay so the popover's focus scope tears down first.
|
||||||
const FOCUS_DELAY_MS = 0;
|
const FOCUS_DELAY_MS = 0;
|
||||||
@@ -52,14 +52,14 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
disabled = false,
|
|
||||||
directory = null,
|
|
||||||
isOpen,
|
|
||||||
query = $bindable(''),
|
|
||||||
customAnchor = null,
|
customAnchor = null,
|
||||||
|
directory = null,
|
||||||
|
disabled = false,
|
||||||
|
isOpen,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
onOpen
|
onOpen,
|
||||||
|
query = $bindable('')
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// File System Access API is opt-in (Chrome / Edge / Opera): the popover
|
// File System Access API is opt-in (Chrome / Edge / Opera): the popover
|
||||||
@@ -88,8 +88,8 @@
|
|||||||
let listContainer = $state<HTMLDivElement | null>(null);
|
let listContainer = $state<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const nav = usePickerNavigation({
|
const nav = usePickerNavigation({
|
||||||
isOpen: () => isOpen,
|
|
||||||
count: () => queryResults.length,
|
count: () => queryResults.length,
|
||||||
|
isOpen: () => isOpen,
|
||||||
onClose: closePicker,
|
onClose: closePicker,
|
||||||
onSelect: (index) => commit(queryResults[index])
|
onSelect: (index) => commit(queryResults[index])
|
||||||
});
|
});
|
||||||
@@ -99,19 +99,24 @@
|
|||||||
// Resolve home eagerly so the chip can abbreviate before the picker opens.
|
// Resolve home eagerly so the chip can abbreviate before the picker opens.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
void toolsStore.resolveServerHome();
|
void toolsStore.resolveServerHome();
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTML `autofocus` is unreliable on dynamically shown elements.
|
// HTML `autofocus` is unreliable on dynamically shown elements.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
setTimeout(() => searchInputRef?.focus(), FOCUS_DELAY_MS);
|
setTimeout(() => searchInputRef?.focus(), FOCUS_DELAY_MS);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
const q = query.trim();
|
const q = query.trim();
|
||||||
|
|
||||||
nav.reset(-1);
|
nav.reset(-1);
|
||||||
|
|
||||||
if (q && fileSearchEnabled) {
|
if (q && fileSearchEnabled) {
|
||||||
search.run(q);
|
search.run(q);
|
||||||
} else {
|
} else {
|
||||||
@@ -124,11 +129,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
useScrollActiveRow({
|
useScrollActiveRow({
|
||||||
getTrigger: () => nav.scrollTrigger,
|
dataIndex: 'result',
|
||||||
getContainer: () => listContainer,
|
getContainer: () => listContainer,
|
||||||
getIndex: () => nav.hoveredIndex,
|
|
||||||
getCount: () => queryResults.length,
|
getCount: () => queryResults.length,
|
||||||
dataIndex: 'result'
|
getIndex: () => nav.hoveredIndex,
|
||||||
|
getTrigger: () => nav.scrollTrigger
|
||||||
});
|
});
|
||||||
|
|
||||||
let searchScope = $state(HOME_TILDE);
|
let searchScope = $state(HOME_TILDE);
|
||||||
@@ -136,16 +141,18 @@
|
|||||||
// An exactly-typed directory is "entered": the shared search lists its
|
// An exactly-typed directory is "entered": the shared search lists its
|
||||||
// children too, so path navigation does not require a trailing slash.
|
// children too, so path navigation does not require a trailing slash.
|
||||||
const search = useDebouncedSearch({
|
const search = useDebouncedSearch({
|
||||||
debounceMs: SEARCH_DEBOUNCE_MS,
|
|
||||||
canRun: () => isOpen && fileSearchEnabled,
|
canRun: () => isOpen && fileSearchEnabled,
|
||||||
|
debounceMs: SEARCH_DEBOUNCE_MS,
|
||||||
getQuery: () => query.trim(),
|
getQuery: () => query.trim(),
|
||||||
run: async (q, signal, isCurrent) => {
|
run: async (q, signal, isCurrent) => {
|
||||||
const trimmed = q.trim();
|
const trimmed = q.trim();
|
||||||
|
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
queryResults = [];
|
queryResults = [];
|
||||||
searchError = null;
|
searchError = null;
|
||||||
nav.reset(-1);
|
nav.reset(-1);
|
||||||
searchScope = homeBase ?? HOME_TILDE;
|
searchScope = homeBase ?? HOME_TILDE;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,25 +167,31 @@
|
|||||||
signal,
|
signal,
|
||||||
{ type: GlobSearchType.DIR }
|
{ type: GlobSearchType.DIR }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
queryResults = [];
|
queryResults = [];
|
||||||
nav.reset(-1);
|
nav.reset(-1);
|
||||||
searchError = res.error;
|
searchError = res.error;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchScope = res.exactDir ?? res.args.path;
|
searchScope = res.exactDir ?? res.args.path;
|
||||||
queryResults = res.entries.map((e) => e.path).slice(0, MAX_RESULTS_SHOWN);
|
queryResults = res.entries.map((e) => e.path).slice(0, MAX_RESULTS_SHOWN);
|
||||||
|
|
||||||
if (queryResults.length > 0) {
|
if (queryResults.length > 0) {
|
||||||
nav.reset(0);
|
nav.reset(0);
|
||||||
nav.bumpScroll(); // scroll the list back to the top (first item is hovered)
|
nav.bumpScroll(); // scroll the list back to the top (first item is hovered)
|
||||||
} else {
|
} else {
|
||||||
nav.reset(-1);
|
nav.reset(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchError = null;
|
searchError = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isCurrent() || signal.aborted) return;
|
if (!isCurrent() || signal.aborted) return;
|
||||||
|
|
||||||
queryResults = [];
|
queryResults = [];
|
||||||
nav.reset(-1);
|
nav.reset(-1);
|
||||||
searchError = err instanceof Error ? err.message : String(err);
|
searchError = err instanceof Error ? err.message : String(err);
|
||||||
@@ -197,7 +210,9 @@
|
|||||||
|
|
||||||
function setDirectory(value: string) {
|
function setDirectory(value: string) {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
|
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
|
|
||||||
onChange?.(trimmed);
|
onChange?.(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,17 +222,18 @@
|
|||||||
async function resolveNativeName(name: string): Promise<string | null> {
|
async function resolveNativeName(name: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
|
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
|
||||||
path: homeBase ?? HOME_TILDE,
|
|
||||||
type: GlobSearchType.DIR,
|
|
||||||
include: buildCaseInsensitiveGlob(name),
|
include: buildCaseInsensitiveGlob(name),
|
||||||
|
limit: NATIVE_LIMIT,
|
||||||
max_depth: NATIVE_MAX_DEPTH,
|
max_depth: NATIVE_MAX_DEPTH,
|
||||||
limit: NATIVE_LIMIT
|
path: homeBase ?? HOME_TILDE,
|
||||||
|
type: GlobSearchType.DIR
|
||||||
});
|
});
|
||||||
const base = typeof res.base === 'string' ? res.base : '';
|
const base = typeof res.base === 'string' ? res.base : '';
|
||||||
const entries = Array.isArray(res.entries) ? (res.entries as GlobEntry[]) : [];
|
const entries = Array.isArray(res.entries) ? (res.entries as GlobEntry[]) : [];
|
||||||
const match = entries.find(
|
const match = entries.find(
|
||||||
(e) => lastPathSegment(e.path).toLowerCase() === name.toLowerCase()
|
(e) => lastPathSegment(e.path).toLowerCase() === name.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
return match ? joinPath(base, match.path) : null;
|
return match ? joinPath(base, match.path) : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -226,9 +242,11 @@
|
|||||||
|
|
||||||
async function browseNative() {
|
async function browseNative() {
|
||||||
if (disabled || !window.showDirectoryPicker) return;
|
if (disabled || !window.showDirectoryPicker) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const handle = await window.showDirectoryPicker();
|
const handle = await window.showDirectoryPicker();
|
||||||
const path = await resolveNativeName(handle.name);
|
const path = await resolveNativeName(handle.name);
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
setDirectory(path);
|
setDirectory(path);
|
||||||
closePicker();
|
closePicker();
|
||||||
@@ -240,16 +258,20 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// user cancelled - silently ignore; other errors are logged
|
// user cancelled - silently ignore; other errors are logged
|
||||||
if (err instanceof DOMException && err.name === 'AbortError') return;
|
if (err instanceof DOMException && err.name === 'AbortError') return;
|
||||||
|
|
||||||
console.error('[ChatFormWorkingDirectory] showDirectoryPicker failed:', err);
|
console.error('[ChatFormWorkingDirectory] showDirectoryPicker failed:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
const value = query.trim();
|
const value = query.trim();
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
closePicker();
|
closePicker();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDirectory(value);
|
setDirectory(value);
|
||||||
closePicker();
|
closePicker();
|
||||||
}
|
}
|
||||||
@@ -257,6 +279,7 @@
|
|||||||
function handleKeydown(event: KeyboardEvent) {
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === KeyboardKey.ENTER) {
|
if (event.key === KeyboardKey.ENTER) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (nav.hoveredIndex >= 0 && queryResults[nav.hoveredIndex]) {
|
if (nav.hoveredIndex >= 0 && queryResults[nav.hoveredIndex]) {
|
||||||
commit(queryResults[nav.hoveredIndex]);
|
commit(queryResults[nav.hoveredIndex]);
|
||||||
} else if (queryResults.length === 0) {
|
} else if (queryResults.length === 0) {
|
||||||
@@ -287,6 +310,7 @@
|
|||||||
function handleDismiss(event?: MouseEvent) {
|
function handleDismiss(event?: MouseEvent) {
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (directory) {
|
if (directory) {
|
||||||
clearDirectory(event);
|
clearDirectory(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Folder, X } from '@lucide/svelte';
|
import { Folder, X } from '@lucide/svelte';
|
||||||
import { abbreviateWorkingDir } from '$lib/utils';
|
|
||||||
import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants';
|
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
|
||||||
import { ActionIcon } from '$lib/components/app/actions';
|
import { ActionIcon } from '$lib/components/app/actions';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants';
|
||||||
|
import { abbreviateWorkingDir } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
directory?: string | null;
|
directory?: string | null;
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
directory = null,
|
directory = null,
|
||||||
homeBase = null,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
showTooltip = false,
|
homeBase = null,
|
||||||
onClear
|
onClear,
|
||||||
|
showTooltip = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const displayLabel = $derived(
|
const displayLabel = $derived(
|
||||||
|
|||||||
+8
-8
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Folder } from '@lucide/svelte';
|
import { Folder } from '@lucide/svelte';
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
import { highlightMatch } from '$lib/utils';
|
|
||||||
import { cn } from '$lib/components/ui/utils';
|
import { cn } from '$lib/components/ui/utils';
|
||||||
|
import { highlightMatch } from '$lib/utils';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
// Fly-in transition for the results list.
|
// Fly-in transition for the results list.
|
||||||
const FLY_Y_PX = -4;
|
const FLY_Y_PX = -4;
|
||||||
@@ -20,21 +20,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
results,
|
container = $bindable(null),
|
||||||
|
error,
|
||||||
hoveredIndex,
|
hoveredIndex,
|
||||||
isSearching,
|
isSearching,
|
||||||
error,
|
|
||||||
rawQuery,
|
|
||||||
container = $bindable(null),
|
|
||||||
onCommit,
|
onCommit,
|
||||||
onHover
|
onHover,
|
||||||
|
rawQuery,
|
||||||
|
results
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="max-h-48 overflow-y-auto py-2"
|
class="max-h-48 overflow-y-auto py-2"
|
||||||
transition:fly={{ y: FLY_Y_PX, duration: FLY_DURATION_MS }}
|
transition:fly={{ duration: FLY_DURATION_MS, y: FLY_Y_PX }}
|
||||||
>
|
>
|
||||||
{#if isSearching && results.length === 0}
|
{#if isSearching && results.length === 0}
|
||||||
<div class="px-2 py-1.5 text-sm text-muted-foreground">Searching...</div>
|
<div class="px-2 py-1.5 text-sm text-muted-foreground">Searching...</div>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
|
|
||||||
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
|
|
||||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
|
||||||
import { DatabaseService } from '$lib/services/database.service';
|
|
||||||
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
|
||||||
import { REASONING_TAGS } from '$lib/constants/agentic';
|
|
||||||
import { MessageRole, AttachmentType, AgenticSectionType } from '$lib/enums';
|
|
||||||
import {
|
import {
|
||||||
ChatMessageAssistant,
|
ChatMessageAssistant,
|
||||||
ChatMessageUser,
|
ChatMessageMcpPrompt,
|
||||||
ChatMessageSystem,
|
|
||||||
ChatMessageSynthetic,
|
ChatMessageSynthetic,
|
||||||
ChatMessageMcpPrompt
|
ChatMessageSystem,
|
||||||
|
ChatMessageUser
|
||||||
} from '$lib/components/app/chat';
|
} from '$lib/components/app/chat';
|
||||||
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
||||||
import { deriveAgenticSections } from '$lib/utils';
|
import { REASONING_TAGS } from '$lib/constants/agentic';
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
|
||||||
import { ROUTES } from '$lib/constants/routes';
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
|
||||||
|
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
|
||||||
|
import { DatabaseService } from '$lib/services/database.service';
|
||||||
|
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
|
||||||
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
|
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||||
|
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||||
|
import { deriveAgenticSections } from '$lib/utils';
|
||||||
|
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -32,12 +32,12 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
message,
|
|
||||||
toolMessages = [],
|
|
||||||
isLastAssistantMessage = false,
|
isLastAssistantMessage = false,
|
||||||
isLastUserMessage = false,
|
isLastUserMessage = false,
|
||||||
|
message,
|
||||||
nextAssistantMessage = null,
|
nextAssistantMessage = null,
|
||||||
siblingInfo = null
|
siblingInfo = null,
|
||||||
|
toolMessages = []
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const chatActions = getChatActionsContext();
|
const chatActions = getChatActionsContext();
|
||||||
@@ -72,10 +72,12 @@
|
|||||||
case AgenticSectionType.REASONING:
|
case AgenticSectionType.REASONING:
|
||||||
case AgenticSectionType.REASONING_PENDING:
|
case AgenticSectionType.REASONING_PENDING:
|
||||||
parts.push(`${REASONING_TAGS.START}\n${section.content}\n${REASONING_TAGS.END}`);
|
parts.push(`${REASONING_TAGS.START}\n${section.content}\n${REASONING_TAGS.END}`);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AgenticSectionType.TEXT:
|
case AgenticSectionType.TEXT:
|
||||||
parts.push(section.content);
|
parts.push(section.content);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AgenticSectionType.TOOL_CALL:
|
case AgenticSectionType.TOOL_CALL:
|
||||||
@@ -115,9 +117,7 @@
|
|||||||
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
|
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
|
||||||
|
|
||||||
setMessageEditContext({
|
setMessageEditContext({
|
||||||
get isEditing() {
|
cancel: handleCancelEdit,
|
||||||
return isEditing;
|
|
||||||
},
|
|
||||||
get editedContent() {
|
get editedContent() {
|
||||||
return editedContent;
|
return editedContent;
|
||||||
},
|
},
|
||||||
@@ -127,6 +127,12 @@
|
|||||||
get editedUploadedFiles() {
|
get editedUploadedFiles() {
|
||||||
return editedUploadedFiles;
|
return editedUploadedFiles;
|
||||||
},
|
},
|
||||||
|
get isEditing() {
|
||||||
|
return isEditing;
|
||||||
|
},
|
||||||
|
get messageRole() {
|
||||||
|
return message.role;
|
||||||
|
},
|
||||||
get originalContent() {
|
get originalContent() {
|
||||||
return message.role === MessageRole.ASSISTANT
|
return message.role === MessageRole.ASSISTANT
|
||||||
? (rawEditContent ?? message.content)
|
? (rawEditContent ?? message.content)
|
||||||
@@ -135,42 +141,40 @@
|
|||||||
get originalExtras() {
|
get originalExtras() {
|
||||||
return message.extra || [];
|
return message.extra || [];
|
||||||
},
|
},
|
||||||
get showSaveOnlyOption() {
|
|
||||||
return showSaveOnlyOption;
|
|
||||||
},
|
|
||||||
get showBranchAfterEditOption() {
|
|
||||||
return showBranchAfterEditOption;
|
|
||||||
},
|
|
||||||
get shouldBranchAfterEdit() {
|
|
||||||
return shouldBranchAfterEdit;
|
|
||||||
},
|
|
||||||
get messageRole() {
|
|
||||||
return message.role;
|
|
||||||
},
|
|
||||||
get rawEditContent() {
|
get rawEditContent() {
|
||||||
return rawEditContent;
|
return rawEditContent;
|
||||||
},
|
},
|
||||||
|
save: handleSaveEdit,
|
||||||
|
saveOnly: handleSaveEditOnly,
|
||||||
setContent: (content: string) => {
|
setContent: (content: string) => {
|
||||||
editedContent = content;
|
editedContent = content;
|
||||||
},
|
},
|
||||||
setExtras: (extras: DatabaseMessageExtra[]) => {
|
setExtras: (extras: DatabaseMessageExtra[]) => {
|
||||||
editedExtras = extras;
|
editedExtras = extras;
|
||||||
},
|
},
|
||||||
setUploadedFiles: (files: ChatUploadedFile[]) => {
|
|
||||||
editedUploadedFiles = files;
|
|
||||||
},
|
|
||||||
setShouldBranchAfterEdit: (value: boolean) => {
|
setShouldBranchAfterEdit: (value: boolean) => {
|
||||||
shouldBranchAfterEdit = value;
|
shouldBranchAfterEdit = value;
|
||||||
},
|
},
|
||||||
save: handleSaveEdit,
|
setUploadedFiles: (files: ChatUploadedFile[]) => {
|
||||||
saveOnly: handleSaveEditOnly,
|
editedUploadedFiles = files;
|
||||||
cancel: handleCancelEdit,
|
},
|
||||||
|
get shouldBranchAfterEdit() {
|
||||||
|
return shouldBranchAfterEdit;
|
||||||
|
},
|
||||||
|
get showBranchAfterEditOption() {
|
||||||
|
return showBranchAfterEditOption;
|
||||||
|
},
|
||||||
|
get showSaveOnlyOption() {
|
||||||
|
return showSaveOnlyOption;
|
||||||
|
},
|
||||||
startEdit: handleEdit
|
startEdit: handleEdit
|
||||||
});
|
});
|
||||||
|
|
||||||
let mcpPromptExtra = $derived.by(() => {
|
let mcpPromptExtra = $derived.by(() => {
|
||||||
if (message.role !== MessageRole.USER) return null;
|
if (message.role !== MessageRole.USER) return null;
|
||||||
|
|
||||||
if (message.content.trim()) return null;
|
if (message.content.trim()) return null;
|
||||||
|
|
||||||
if (!message.extra || message.extra.length !== 1) return null;
|
if (!message.extra || message.extra.length !== 1) return null;
|
||||||
|
|
||||||
const extra = message.extra[0];
|
const extra = message.extra[0];
|
||||||
@@ -238,6 +242,7 @@
|
|||||||
|
|
||||||
function handleEdit() {
|
function handleEdit() {
|
||||||
isEditing = true;
|
isEditing = true;
|
||||||
|
|
||||||
// Clear temporary placeholder content for system messages
|
// Clear temporary placeholder content for system messages
|
||||||
if (message.role === MessageRole.SYSTEM && message.content === SYSTEM_MESSAGE_PLACEHOLDER) {
|
if (message.role === MessageRole.SYSTEM && message.content === SYSTEM_MESSAGE_PLACEHOLDER) {
|
||||||
editedContent = '';
|
editedContent = '';
|
||||||
@@ -281,6 +286,7 @@
|
|||||||
// After the system message flow ends, hand focus to the main chat form
|
// After the system message flow ends, hand focus to the main chat form
|
||||||
function focusMainChatForm() {
|
function focusMainChatForm() {
|
||||||
if (isMobile.current) return;
|
if (isMobile.current) return;
|
||||||
|
|
||||||
document.querySelector<HTMLTextAreaElement>('.chat-screen-form-wrapper textarea')?.focus();
|
document.querySelector<HTMLTextAreaElement>('.chat-screen-form-wrapper textarea')?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,23 +298,29 @@
|
|||||||
// If content is empty, remove without deleting children
|
// If content is empty, remove without deleting children
|
||||||
if (!newContent) {
|
if (!newContent) {
|
||||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||||
|
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
|
|
||||||
if (conversationDeleted) {
|
if (conversationDeleted) {
|
||||||
goto(ROUTES.START);
|
goto(ROUTES.START);
|
||||||
} else {
|
} else {
|
||||||
focusMainChatForm();
|
focusMainChatForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DatabaseService.updateMessage(message.id, { content: newContent });
|
await DatabaseService.updateMessage(message.id, { content: newContent });
|
||||||
const index = conversationsStore.findMessageIndex(message.id);
|
const index = conversationsStore.findMessageIndex(message.id);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
conversationsStore.updateMessageAtIndex(index, { content: newContent });
|
conversationsStore.updateMessageAtIndex(index, { content: newContent });
|
||||||
}
|
}
|
||||||
|
|
||||||
focusMainChatForm();
|
focusMainChatForm();
|
||||||
} else if (message.role === MessageRole.USER) {
|
} else if (message.role === MessageRole.USER) {
|
||||||
const finalExtras = await getMergedExtras();
|
const finalExtras = await getMergedExtras();
|
||||||
|
|
||||||
chatActions.editWithBranching(message, editedContent.trim(), finalExtras);
|
chatActions.editWithBranching(message, editedContent.trim(), finalExtras);
|
||||||
} else {
|
} else {
|
||||||
// For assistant messages, preserve exact content including trailing whitespace
|
// For assistant messages, preserve exact content including trailing whitespace
|
||||||
@@ -325,6 +337,7 @@
|
|||||||
if (message.role === MessageRole.USER) {
|
if (message.role === MessageRole.USER) {
|
||||||
// For user messages, trim to avoid accidental whitespace
|
// For user messages, trim to avoid accidental whitespace
|
||||||
const finalExtras = await getMergedExtras();
|
const finalExtras = await getMergedExtras();
|
||||||
|
|
||||||
chatActions.editUserMessagePreserveResponses(message, editedContent.trim(), finalExtras);
|
chatActions.editUserMessagePreserveResponses(message, editedContent.trim(), finalExtras);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-9
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
ChatMessageAgenticContent,
|
|
||||||
ChatMessageActionIcons,
|
ChatMessageActionIcons,
|
||||||
|
ChatMessageAgenticContent,
|
||||||
ChatMessageAssistantModel,
|
ChatMessageAssistantModel,
|
||||||
ChatMessageAssistantProcessingInfo,
|
ChatMessageAssistantProcessingInfo,
|
||||||
ChatMessageAssistantRawOutput,
|
ChatMessageAssistantRawOutput,
|
||||||
@@ -9,14 +9,13 @@
|
|||||||
ChatMessageEditForm
|
ChatMessageEditForm
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getMessageEditContext } from '$lib/contexts';
|
||||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
|
||||||
import { chatStore, isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
|
|
||||||
import { modelLoadProgressText } from '$lib/utils';
|
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
|
||||||
import { modelsStore } from '$lib/stores/models.svelte';
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||||
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import { modelLoadProgressText } from '$lib/utils';
|
||||||
import { hasAgenticContent } from '$lib/utils';
|
import { hasAgenticContent } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -49,7 +48,6 @@
|
|||||||
deletionInfo,
|
deletionInfo,
|
||||||
isLastAssistantMessage = false,
|
isLastAssistantMessage = false,
|
||||||
message,
|
message,
|
||||||
toolMessages = [],
|
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onContinue,
|
onContinue,
|
||||||
onCopy,
|
onCopy,
|
||||||
@@ -61,7 +59,8 @@
|
|||||||
onShowDeleteDialogChange,
|
onShowDeleteDialogChange,
|
||||||
showDeleteDialog,
|
showDeleteDialog,
|
||||||
siblingInfo = null,
|
siblingInfo = null,
|
||||||
textareaElement = $bindable()
|
textareaElement = $bindable(),
|
||||||
|
toolMessages = []
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Get edit context
|
// Get edit context
|
||||||
@@ -124,18 +123,21 @@
|
|||||||
|
|
||||||
if (!userMessageEl) {
|
if (!userMessageEl) {
|
||||||
lastUserMessageHeight = 0;
|
lastUserMessageHeight = 0;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateHeight = () => {
|
const updateHeight = () => {
|
||||||
const rect = userMessageEl.getBoundingClientRect();
|
const rect = userMessageEl.getBoundingClientRect();
|
||||||
const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
|
const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
|
||||||
|
|
||||||
lastUserMessageHeight = Math.round(rect.height + marginTop);
|
lastUserMessageHeight = Math.round(rect.height + marginTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateHeight();
|
updateHeight();
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(updateHeight);
|
const resizeObserver = new ResizeObserver(updateHeight);
|
||||||
|
|
||||||
resizeObserver.observe(userMessageEl);
|
resizeObserver.observe(userMessageEl);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
+4
-3
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ModelBadge, ModelsSelectorDropdown } from '$lib/components/app';
|
import { ModelBadge, ModelsSelectorDropdown } from '$lib/components/app';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
|
||||||
import { modelsStore } from '$lib/stores/models.svelte';
|
|
||||||
import { ServerModelStatus } from '$lib/enums';
|
import { ServerModelStatus } from '$lib/enums';
|
||||||
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
displayedModel: string | null;
|
displayedModel: string | null;
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
onRegenerate: (modelOverride?: string) => void;
|
onRegenerate: (modelOverride?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { displayedModel, isRouter, isLoading, onRegenerate }: Props = $props();
|
let { displayedModel, isLoading, isRouter, onRegenerate }: Props = $props();
|
||||||
|
|
||||||
let pendingModel = $state<string | null>(null);
|
let pendingModel = $state<string | null>(null);
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRegenerate(modelName);
|
onRegenerate(modelName);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import type { UseProcessingStateReturn } from '$lib/hooks/use-processing-state.svelte';
|
import type { UseProcessingStateReturn } from '$lib/hooks/use-processing-state.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelLoadingText: string | null;
|
modelLoadingText: string | null;
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
position: 'top' | 'bottom';
|
position: 'top' | 'bottom';
|
||||||
}
|
}
|
||||||
|
|
||||||
let { modelLoadingText, processingState, position }: Props = $props();
|
let { modelLoadingText, position, processingState }: Props = $props();
|
||||||
|
|
||||||
const marginClass = position === 'top' ? 'mt-6' : 'mt-4';
|
const marginClass = position === 'top' ? 'mt-6' : 'mt-4';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { deriveAgenticSections, buildAssistantRawOutput } from '$lib/utils';
|
import { buildAssistantRawOutput, deriveAgenticSections } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
let rawOutputContent = $derived.by(() => {
|
let rawOutputContent = $derived.by(() => {
|
||||||
const sections = deriveAgenticSections(message, toolMessages, [], false);
|
const sections = deriveAgenticSections(message, toolMessages, [], false);
|
||||||
|
|
||||||
return buildAssistantRawOutput(sections);
|
return buildAssistantRawOutput(sections);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
showMessageStats: boolean;
|
showMessageStats: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { message, isLoading, processingState, showMessageStats }: Props = $props();
|
let { isLoading, message, processingState, showMessageStats }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
|
{#if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Folder, FolderX } from '@lucide/svelte';
|
import { Folder, FolderX } from '@lucide/svelte';
|
||||||
import { parseCwdMessage } from '$lib/utils';
|
|
||||||
import type { DatabaseMessage } from '$lib/types';
|
import type { DatabaseMessage } from '$lib/types';
|
||||||
|
import { parseCwdMessage } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
+9
-9
@@ -5,7 +5,7 @@
|
|||||||
ChatMessageMcpPromptContent
|
ChatMessageMcpPromptContent
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getMessageEditContext } from '$lib/contexts';
|
||||||
import { MessageRole, McpPromptVariant } from '$lib/enums';
|
import { McpPromptVariant, MessageRole } from '$lib/enums';
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -30,17 +30,17 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
message,
|
|
||||||
mcpPrompt,
|
|
||||||
siblingInfo = null,
|
|
||||||
showDeleteDialog,
|
|
||||||
deletionInfo,
|
deletionInfo,
|
||||||
onCopy,
|
mcpPrompt,
|
||||||
onEdit,
|
message,
|
||||||
onDelete,
|
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
|
onCopy,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
onNavigateToSibling,
|
onNavigateToSibling,
|
||||||
onShowDeleteDialogChange
|
onShowDeleteDialogChange,
|
||||||
|
showDeleteDialog,
|
||||||
|
siblingInfo = null
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Get edit context
|
// Get edit context
|
||||||
|
|||||||
+16
-13
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Card } from '$lib/components/ui/card';
|
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
|
||||||
import { McpPromptVariant } from '$lib/enums';
|
|
||||||
import { TruncatedText } from '$lib/components/app/misc';
|
import { TruncatedText } from '$lib/components/app/misc';
|
||||||
|
import { Card } from '$lib/components/ui/card';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { McpPromptVariant } from '$lib/enums';
|
||||||
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
|
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
interface ContentPart {
|
interface ContentPart {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
prompt,
|
|
||||||
variant = McpPromptVariant.MESSAGE,
|
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
loadError
|
loadError,
|
||||||
|
prompt,
|
||||||
|
variant = McpPromptVariant.MESSAGE
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let hoveredArgKey = $state<string | null>(null);
|
let hoveredArgKey = $state<string | null>(null);
|
||||||
@@ -35,13 +35,15 @@
|
|||||||
|
|
||||||
let contentParts = $derived.by((): ContentPart[] => {
|
let contentParts = $derived.by((): ContentPart[] => {
|
||||||
if (!prompt.content || !hasArguments) {
|
if (!prompt.content || !hasArguments) {
|
||||||
return [{ text: prompt.content || '', argKey: null }];
|
return [{ argKey: null, text: prompt.content || '' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts: ContentPart[] = [];
|
const parts: ContentPart[] = [];
|
||||||
|
|
||||||
let remaining = prompt.content;
|
let remaining = prompt.content;
|
||||||
|
|
||||||
const valueToKey = new SvelteMap<string, string>();
|
const valueToKey = new SvelteMap<string, string>();
|
||||||
|
|
||||||
for (const [key, value] of argumentEntries) {
|
for (const [key, value] of argumentEntries) {
|
||||||
if (value && value.trim()) {
|
if (value && value.trim()) {
|
||||||
valueToKey.set(value, key);
|
valueToKey.set(value, key);
|
||||||
@@ -55,20 +57,21 @@
|
|||||||
|
|
||||||
for (const value of sortedValues) {
|
for (const value of sortedValues) {
|
||||||
const index = remaining.indexOf(value);
|
const index = remaining.indexOf(value);
|
||||||
|
|
||||||
if (index !== -1 && (earliestMatch === null || index < earliestMatch.index)) {
|
if (index !== -1 && (earliestMatch === null || index < earliestMatch.index)) {
|
||||||
earliestMatch = { index, value, key: valueToKey.get(value)! };
|
earliestMatch = { index, key: valueToKey.get(value)!, value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (earliestMatch) {
|
if (earliestMatch) {
|
||||||
if (earliestMatch.index > 0) {
|
if (earliestMatch.index > 0) {
|
||||||
parts.push({ text: remaining.slice(0, earliestMatch.index), argKey: null });
|
parts.push({ argKey: null, text: remaining.slice(0, earliestMatch.index) });
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push({ text: earliestMatch.value, argKey: earliestMatch.key });
|
parts.push({ argKey: earliestMatch.key, text: earliestMatch.value });
|
||||||
remaining = remaining.slice(earliestMatch.index + earliestMatch.value.length);
|
remaining = remaining.slice(earliestMatch.index + earliestMatch.value.length);
|
||||||
} else {
|
} else {
|
||||||
parts.push({ text: remaining, argKey: null });
|
parts.push({ argKey: null, text: remaining });
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { parseCwdMessage } from '$lib/utils';
|
|
||||||
import type { DatabaseMessage } from '$lib/types';
|
|
||||||
import ChatMessageCwdChange from './ChatMessageCwdChange.svelte';
|
import ChatMessageCwdChange from './ChatMessageCwdChange.svelte';
|
||||||
|
import type { DatabaseMessage } from '$lib/types';
|
||||||
|
import { parseCwdMessage } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
+6
-6
@@ -31,16 +31,16 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
message,
|
|
||||||
siblingInfo = null,
|
|
||||||
showDeleteDialog,
|
|
||||||
deletionInfo,
|
deletionInfo,
|
||||||
onCopy,
|
message,
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
|
onCopy,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
onNavigateToSibling,
|
onNavigateToSibling,
|
||||||
onShowDeleteDialogChange,
|
onShowDeleteDialogChange,
|
||||||
|
showDeleteDialog,
|
||||||
|
siblingInfo = null,
|
||||||
textareaElement = $bindable()
|
textareaElement = $bindable()
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -1,12 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { BuiltInTool } from '$lib/enums';
|
|
||||||
import {
|
|
||||||
extractSearchQuery,
|
|
||||||
extractSearchResults,
|
|
||||||
isWebSearchToolName,
|
|
||||||
type AgenticSection
|
|
||||||
} from '$lib/utils';
|
|
||||||
import type { DatabaseMessageExtra } from '$lib/types';
|
|
||||||
import ChatMessageToolCallBlockDefault from './ChatMessageToolCallBlockDefault.svelte';
|
import ChatMessageToolCallBlockDefault from './ChatMessageToolCallBlockDefault.svelte';
|
||||||
import ChatMessageToolCallBlockEditFile from './ChatMessageToolCallBlockEditFile.svelte';
|
import ChatMessageToolCallBlockEditFile from './ChatMessageToolCallBlockEditFile.svelte';
|
||||||
import ChatMessageToolCallBlockExecShellCommand from './ChatMessageToolCallBlockExecShellCommand.svelte';
|
import ChatMessageToolCallBlockExecShellCommand from './ChatMessageToolCallBlockExecShellCommand.svelte';
|
||||||
@@ -18,6 +10,14 @@
|
|||||||
import ChatMessageToolCallBlockRunJavascript from './ChatMessageToolCallBlockRunJavascript.svelte';
|
import ChatMessageToolCallBlockRunJavascript from './ChatMessageToolCallBlockRunJavascript.svelte';
|
||||||
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
|
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
|
||||||
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
|
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
|
||||||
|
import { BuiltInTool } from '$lib/enums';
|
||||||
|
import type { DatabaseMessageExtra } from '$lib/types';
|
||||||
|
import {
|
||||||
|
type AgenticSection,
|
||||||
|
extractSearchQuery,
|
||||||
|
extractSearchResults,
|
||||||
|
isWebSearchToolName
|
||||||
|
} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, attachments, open, isStreaming, isExecuting, onToggle }: Props = $props();
|
let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const searchResults = $derived(extractSearchResults(section.toolResult));
|
const searchResults = $derived(extractSearchResults(section.toolResult));
|
||||||
const searchQuery = $derived(extractSearchQuery(section.toolArgs));
|
const searchQuery = $derived(extractSearchQuery(section.toolArgs));
|
||||||
|
|||||||
+7
-7
@@ -3,19 +3,19 @@
|
|||||||
// Renders section.toolArgs / section.toolResult directly using the
|
// Renders section.toolArgs / section.toolResult directly using the
|
||||||
// shared chrome shell.
|
// shared chrome shell.
|
||||||
|
|
||||||
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
import { Loader2 } from '@lucide/svelte';
|
import { Loader2 } from '@lucide/svelte';
|
||||||
import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
|
import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
|
||||||
import { FileTypeText, ToolResultKind } from '$lib/enums';
|
|
||||||
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||||
|
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
||||||
|
import { FileTypeText, ToolResultKind } from '$lib/enums';
|
||||||
|
import type { DatabaseMessageExtra } from '$lib/types';
|
||||||
import {
|
import {
|
||||||
|
type AgenticSection,
|
||||||
classifyToolResult,
|
classifyToolResult,
|
||||||
formatJsonPretty,
|
formatJsonPretty,
|
||||||
parseToolResultWithImages,
|
parseToolResultWithImages
|
||||||
type AgenticSection
|
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
|
||||||
import type { DatabaseMessageExtra } from '$lib/types';
|
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, attachments, onToggle }: Props = $props();
|
let { attachments, isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
|
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
|
||||||
const outputKind = $derived(classifyToolResult(section.toolResult));
|
const outputKind = $derived(classifyToolResult(section.toolResult));
|
||||||
|
|||||||
+5
-5
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { XCircle } from '@lucide/svelte';
|
|
||||||
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
|
||||||
import { computeLineDiff, prefixFor, abbreviateHome, type AgenticSection } from '$lib/utils';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import { parseEditFileMeta } from './parsers/edit-file';
|
import { parseEditFileMeta } from './parsers/edit-file';
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
|
import { XCircle } from '@lucide/svelte';
|
||||||
|
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import { abbreviateHome, type AgenticSection, computeLineDiff, prefixFor } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, onToggle }: Props = $props();
|
let { isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const editFileMeta = $derived(parseEditFileMeta(section));
|
const editFileMeta = $derived(parseEditFileMeta(section));
|
||||||
const home = $derived(toolsStore.serverHome);
|
const home = $derived(toolsStore.serverHome);
|
||||||
|
|||||||
+19
-11
@@ -6,26 +6,26 @@
|
|||||||
// The scroll-to-bottom auto-scroll logic mirrors what was here
|
// The scroll-to-bottom auto-scroll logic mirrors what was here
|
||||||
// before extraction.
|
// before extraction.
|
||||||
|
|
||||||
import { Check, Loader2, XCircle, AlertTriangle } from '@lucide/svelte';
|
import { parseExecShellCommandMeta } from './parsers/exec-shell-command';
|
||||||
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
|
import { AlertTriangle, Check, Loader2, XCircle } from '@lucide/svelte';
|
||||||
import { CollapsibleTerminalBlock } from '$lib/components/app';
|
import { CollapsibleTerminalBlock } from '$lib/components/app';
|
||||||
import { SETTINGS_KEYS } from '$lib/constants';
|
import { SETTINGS_KEYS } from '$lib/constants';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
|
||||||
import { TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
|
import { TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
|
||||||
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import type { DatabaseMessageExtra } from '$lib/types';
|
||||||
import {
|
import {
|
||||||
abbreviateHome,
|
abbreviateHome,
|
||||||
|
type AgenticSection,
|
||||||
|
type ExecShellExitStatus,
|
||||||
highlightCode,
|
highlightCode,
|
||||||
isExitCodeSummaryLine,
|
isExitCodeSummaryLine,
|
||||||
parseExecShellCommandError,
|
parseExecShellCommandError,
|
||||||
parseExecShellCommandExitStatus,
|
parseExecShellCommandExitStatus,
|
||||||
parseToolResultWithImages,
|
parseToolResultWithImages,
|
||||||
type AgenticSection,
|
|
||||||
type ExecShellExitStatus,
|
|
||||||
type ToolResultLine
|
type ToolResultLine
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import { parseExecShellCommandMeta } from './parsers/exec-shell-command';
|
|
||||||
import type { DatabaseMessageExtra } from '$lib/types';
|
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, isExecuting = false, attachments, onToggle }: Props = $props();
|
let { attachments, isExecuting = false, isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
// `isLive` covers all in-flight phases: pre-chunk spinner and
|
// `isLive` covers all in-flight phases: pre-chunk spinner and
|
||||||
// streaming itself. Frozen output (tool done while agent continues)
|
// streaming itself. Frozen output (tool done while agent continues)
|
||||||
@@ -108,6 +108,7 @@
|
|||||||
|
|
||||||
function isAtBottom(): boolean {
|
function isAtBottom(): boolean {
|
||||||
if (!scrollEl) return false;
|
if (!scrollEl) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
|
scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
|
||||||
SCROLL_BOTTOM_THRESHOLD_PX
|
SCROLL_BOTTOM_THRESHOLD_PX
|
||||||
@@ -116,6 +117,7 @@
|
|||||||
|
|
||||||
function scrollToBottomOnFrame() {
|
function scrollToBottomOnFrame() {
|
||||||
if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
|
if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
|
||||||
|
|
||||||
pendingFrame = requestAnimationFrame(() => {
|
pendingFrame = requestAnimationFrame(() => {
|
||||||
pendingFrame = null;
|
pendingFrame = null;
|
||||||
|
|
||||||
@@ -128,18 +130,23 @@
|
|||||||
|
|
||||||
function handleScrollEvent() {
|
function handleScrollEvent() {
|
||||||
if (!scrollEl) return;
|
if (!scrollEl) return;
|
||||||
|
|
||||||
const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
|
const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
|
||||||
|
|
||||||
if (isScrollingUp && !isAtBottom()) {
|
if (isScrollingUp && !isAtBottom()) {
|
||||||
userScrolledUp = true;
|
userScrolledUp = true;
|
||||||
} else if (isAtBottom()) {
|
} else if (isAtBottom()) {
|
||||||
userScrolledUp = false;
|
userScrolledUp = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastScrollTop = scrollEl.scrollTop;
|
lastScrollTop = scrollEl.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
void section.toolResult;
|
void section.toolResult;
|
||||||
|
|
||||||
if (!scrollEl || !autoScroll) return;
|
if (!scrollEl || !autoScroll) return;
|
||||||
|
|
||||||
scrollToBottomOnFrame();
|
scrollToBottomOnFrame();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,10 +156,11 @@
|
|||||||
if (!scrollEl || !autoScroll) return;
|
if (!scrollEl || !autoScroll) return;
|
||||||
|
|
||||||
const observer = new MutationObserver(() => scrollToBottomOnFrame());
|
const observer = new MutationObserver(() => scrollToBottomOnFrame());
|
||||||
|
|
||||||
observer.observe(scrollEl, {
|
observer.observe(scrollEl, {
|
||||||
|
characterData: true,
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true
|
||||||
characterData: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
|
|||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { XCircle } from '@lucide/svelte';
|
|
||||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import { parseFileGlobSearchMeta } from './parsers/file-glob-search';
|
import { parseFileGlobSearchMeta } from './parsers/file-glob-search';
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
|
import { XCircle } from '@lucide/svelte';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, onToggle }: Props = $props();
|
let { isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const fileGlobMeta = $derived(parseFileGlobSearchMeta(section));
|
const fileGlobMeta = $derived(parseFileGlobSearchMeta(section));
|
||||||
const home = $derived(toolsStore.serverHome);
|
const home = $derived(toolsStore.serverHome);
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,7 @@
|
|||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, isStreaming = false }: Props = $props();
|
let { isStreaming = false, section }: Props = $props();
|
||||||
|
|
||||||
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
||||||
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
|
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
|
||||||
@@ -24,9 +24,12 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(toolResultString);
|
const parsed: unknown = JSON.parse(toolResultString);
|
||||||
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
const obj = parsed as Record<string, unknown>;
|
const obj = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
if (typeof obj.error === 'string') return { errorMessage: obj.error };
|
if (typeof obj.error === 'string') return { errorMessage: obj.error };
|
||||||
|
|
||||||
if (typeof obj.result === 'string') return { dateString: obj.result.trim() };
|
if (typeof obj.result === 'string') return { dateString: obj.result.trim() };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+7
-4
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Info, Loader2 } from '@lucide/svelte';
|
import { Info, Loader2 } from '@lucide/svelte';
|
||||||
import { AgenticSectionType } from '$lib/enums';
|
import { AgenticSectionType } from '$lib/enums';
|
||||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, isStreaming = false }: Props = $props();
|
let { isStreaming = false, section }: Props = $props();
|
||||||
|
|
||||||
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
||||||
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
|
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
|
||||||
@@ -26,12 +26,15 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(toolResultString);
|
const parsed: unknown = JSON.parse(toolResultString);
|
||||||
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
const obj = parsed as Record<string, unknown>;
|
const obj = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
if (typeof obj.error === 'string') return { errorMessage: obj.error };
|
if (typeof obj.error === 'string') return { errorMessage: obj.error };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
os: typeof obj.os === 'string' ? obj.os : undefined,
|
cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined,
|
||||||
cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined
|
os: typeof obj.os === 'string' ? obj.os : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { XCircle } from '@lucide/svelte';
|
|
||||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
|
||||||
import { parseGrepSearchMeta } from './parsers/grep-search';
|
import { parseGrepSearchMeta } from './parsers/grep-search';
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
|
import { XCircle } from '@lucide/svelte';
|
||||||
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
|
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, onToggle }: Props = $props();
|
let { isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const grepMeta = $derived(parseGrepSearchMeta(section));
|
const grepMeta = $derived(parseGrepSearchMeta(section));
|
||||||
const home = $derived(toolsStore.serverHome);
|
const home = $derived(toolsStore.serverHome);
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { parseReadFileMeta } from './parsers/read-file';
|
||||||
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||||
import { DEFAULT_LANGUAGE, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
import { DEFAULT_LANGUAGE, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||||
import { type AgenticSection } from '$lib/utils';
|
import { type AgenticSection } from '$lib/utils';
|
||||||
import { parseReadFileMeta } from './parsers/read-file';
|
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, onToggle }: Props = $props();
|
let { isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const readFileMeta = $derived(parseReadFileMeta(section));
|
const readFileMeta = $derived(parseReadFileMeta(section));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+6
-6
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { XCircle, Terminal } from '@lucide/svelte';
|
|
||||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
|
||||||
import { FileTypeText } from '$lib/enums';
|
|
||||||
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
|
||||||
import { getBuiltinToolUi, type AgenticSection } from '$lib/utils';
|
|
||||||
import { parseRunJavascriptMeta } from './parsers/run-javascript';
|
import { parseRunJavascriptMeta } from './parsers/run-javascript';
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
|
import { Terminal, XCircle } from '@lucide/svelte';
|
||||||
|
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||||
|
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||||
|
import { FileTypeText } from '$lib/enums';
|
||||||
|
import { type AgenticSection, getBuiltinToolUi } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, onToggle }: Props = $props();
|
let { isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const runJsMeta = $derived(parseRunJavascriptMeta(section));
|
const runJsMeta = $derived(parseRunJavascriptMeta(section));
|
||||||
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
|
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
|
||||||
|
|||||||
+11
-7
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
|
|
||||||
import { Globe, Loader2 } from '@lucide/svelte';
|
import { Globe, Loader2 } from '@lucide/svelte';
|
||||||
import { CollapsibleContentBlock } from '$lib/components/app';
|
import { CollapsibleContentBlock } from '$lib/components/app';
|
||||||
import * as HoverCard from '$lib/components/ui/hover-card';
|
import * as HoverCard from '$lib/components/ui/hover-card';
|
||||||
|
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
|
||||||
import { AgenticSectionType } from '$lib/enums';
|
import { AgenticSectionType } from '$lib/enums';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
import {
|
import {
|
||||||
extractSearchResults,
|
type AgenticSection,
|
||||||
extractSearchQuery,
|
extractSearchQuery,
|
||||||
|
extractSearchResults,
|
||||||
faviconForUrl,
|
faviconForUrl,
|
||||||
sanitizeExternalUrl,
|
sanitizeExternalUrl,
|
||||||
type SearchResult,
|
type SearchResult
|
||||||
type AgenticSection
|
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open = $bindable(false), isStreaming = false, onToggle }: Props = $props();
|
let { isStreaming = false, onToggle, open = $bindable(false), section }: Props = $props();
|
||||||
|
|
||||||
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
||||||
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
|
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
// retrospective.
|
// retrospective.
|
||||||
const title = $derived.by(() => {
|
const title = $derived.by(() => {
|
||||||
const verb = showSpinner ? 'Searching' : 'Searched';
|
const verb = showSpinner ? 'Searching' : 'Searched';
|
||||||
|
|
||||||
return query ? `${verb} web for "${query}"` : `${verb} web`;
|
return query ? `${verb} web for "${query}"` : `${verb} web`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,13 +53,16 @@
|
|||||||
|
|
||||||
function formatPublishDate(iso: string | undefined): string | null {
|
function formatPublishDate(iso: string | undefined): string | null {
|
||||||
if (!iso) return null;
|
if (!iso) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(iso);
|
const date = new Date(iso);
|
||||||
|
|
||||||
if (Number.isNaN(date.getTime())) return iso;
|
if (Number.isNaN(date.getTime())) return iso;
|
||||||
|
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(undefined, {
|
||||||
year: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric'
|
year: 'numeric'
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return iso;
|
return iso;
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { parseWriteFileMeta } from './parsers/write-file';
|
||||||
|
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||||
import { XCircle } from '@lucide/svelte';
|
import { XCircle } from '@lucide/svelte';
|
||||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||||
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
||||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
|
||||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||||
import { parseWriteFileMeta } from './parsers/write-file';
|
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { section, open, isStreaming, onToggle }: Props = $props();
|
let { isStreaming, onToggle, open, section }: Props = $props();
|
||||||
|
|
||||||
const writeFileMeta = $derived(parseWriteFileMeta(section));
|
const writeFileMeta = $derived(parseWriteFileMeta(section));
|
||||||
const home = $derived(toolsStore.serverHome);
|
const home = $derived(toolsStore.serverHome);
|
||||||
|
|||||||
+14
-11
@@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
import { Loader2, Wrench } from '@lucide/svelte';
|
import { Loader2, Wrench } from '@lucide/svelte';
|
||||||
import { CollapsibleContentBlock } from '$lib/components/app';
|
import { CollapsibleContentBlock } from '$lib/components/app';
|
||||||
|
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
||||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
|
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
|
||||||
import { AgenticSectionType } from '$lib/enums';
|
import { AgenticSectionType } from '$lib/enums';
|
||||||
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
import type { Component, Snippet } from 'svelte';
|
|
||||||
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
|
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
|
||||||
|
import type { Component, Snippet } from 'svelte';
|
||||||
|
|
||||||
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
|
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
|
||||||
|
|
||||||
@@ -64,17 +64,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
section,
|
children,
|
||||||
open,
|
extraLiveStreaming = false,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
meta,
|
meta,
|
||||||
extraLiveStreaming = false,
|
onToggle,
|
||||||
|
open,
|
||||||
|
section,
|
||||||
spinIconWhenActive = false,
|
spinIconWhenActive = false,
|
||||||
wrapper: Wrapper = CollapsibleContentBlock,
|
|
||||||
title,
|
title,
|
||||||
titleSnippet,
|
titleSnippet,
|
||||||
onToggle,
|
wrapper: Wrapper = CollapsibleContentBlock
|
||||||
children
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
|
||||||
@@ -102,8 +102,11 @@
|
|||||||
// signals activity; only terminal states get a pill.
|
// signals activity; only terminal states get a pill.
|
||||||
function subtitleFor(errorMessage?: string): string | undefined {
|
function subtitleFor(errorMessage?: string): string | undefined {
|
||||||
if (showSpinner) return undefined;
|
if (showSpinner) return undefined;
|
||||||
|
|
||||||
if (errorMessage) return 'failed';
|
if (errorMessage) return 'failed';
|
||||||
|
|
||||||
if (isStreamingCall && !isStreaming) return 'incomplete';
|
if (isStreamingCall && !isStreaming) return 'incomplete';
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +125,9 @@
|
|||||||
{onToggle}
|
{onToggle}
|
||||||
>
|
>
|
||||||
{@render children(meta, {
|
{@render children(meta, {
|
||||||
isStreaming,
|
isCodeStreaming,
|
||||||
isPending,
|
isPending,
|
||||||
isStreamingCall,
|
isStreaming,
|
||||||
isCodeStreaming
|
isStreamingCall
|
||||||
})}
|
})}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|||||||
+4
-1
@@ -5,8 +5,8 @@
|
|||||||
// stay focused on its own format quirks.
|
// stay focused on its own format quirks.
|
||||||
|
|
||||||
import { BuiltInTool } from '$lib/enums';
|
import { BuiltInTool } from '$lib/enums';
|
||||||
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
|
|
||||||
import type { AgenticSection } from '$lib/utils/agentic';
|
import type { AgenticSection } from '$lib/utils/agentic';
|
||||||
|
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strict (final-state) JSON parser for a tool-args blob. Mirrors the
|
* Strict (final-state) JSON parser for a tool-args blob. Mirrors the
|
||||||
@@ -17,9 +17,11 @@ import type { AgenticSection } from '$lib/utils/agentic';
|
|||||||
function parseFinalToolArgs(blob: string): Record<string, unknown> | null {
|
function parseFinalToolArgs(blob: string): Record<string, unknown> | null {
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(blob);
|
const parsed: unknown = JSON.parse(blob);
|
||||||
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
return parsed as Record<string, unknown>;
|
return parsed as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -43,6 +45,7 @@ export function parseToolArgs(
|
|||||||
options: { partial?: boolean } = {}
|
options: { partial?: boolean } = {}
|
||||||
): Record<string, unknown> | null {
|
): Record<string, unknown> | null {
|
||||||
if (section.toolName !== expected || !section.toolArgs) return null;
|
if (section.toolName !== expected || !section.toolArgs) return null;
|
||||||
|
|
||||||
return options.partial
|
return options.partial
|
||||||
? parsePartialJsonArgs(section.toolArgs)
|
? parsePartialJsonArgs(section.toolArgs)
|
||||||
: parseFinalToolArgs(section.toolArgs);
|
: parseFinalToolArgs(section.toolArgs);
|
||||||
|
|||||||
+18
-9
@@ -3,10 +3,10 @@
|
|||||||
// rendering), plus the result blob for `result` / `edits_applied` /
|
// rendering), plus the result blob for `result` / `edits_applied` /
|
||||||
// `error` fields.
|
// `error` fields.
|
||||||
|
|
||||||
import { BuiltInTool } from '$lib/enums';
|
|
||||||
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
|
|
||||||
import { tryParseToolResultObject, type AgenticSection } from '$lib/utils';
|
|
||||||
import { parseToolArgs } from './_shared';
|
import { parseToolArgs } from './_shared';
|
||||||
|
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
|
||||||
|
import { BuiltInTool } from '$lib/enums';
|
||||||
|
import { type AgenticSection, tryParseToolResultObject } from '$lib/utils';
|
||||||
|
|
||||||
export type EditFileEdit = {
|
export type EditFileEdit = {
|
||||||
oldText: string;
|
oldText: string;
|
||||||
@@ -24,48 +24,57 @@ export type EditFileMeta = {
|
|||||||
|
|
||||||
export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
|
export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.EDIT_FILE, section, { partial: true });
|
const args = parseToolArgs(BuiltInTool.EDIT_FILE, section, { partial: true });
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
||||||
|
|
||||||
if (typeof rawPath !== 'string' || !rawPath) return null;
|
if (typeof rawPath !== 'string' || !rawPath) return null;
|
||||||
|
|
||||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||||
|
|
||||||
// Filter the streamed edits array strictly: each entry must be an
|
// Filter the streamed edits array strictly: each entry must be an
|
||||||
// object with a non-empty `old_text`. Edits without an old_text
|
// object with a non-empty `old_text`. Edits without an old_text
|
||||||
// would diff against empty and render as a full re-write.
|
// would diff against empty and render as a full re-write.
|
||||||
const rawEdits = Array.isArray(args.edits) ? args.edits : [];
|
const rawEdits = Array.isArray(args.edits) ? args.edits : [];
|
||||||
const edits: EditFileEdit[] = [];
|
const edits: EditFileEdit[] = [];
|
||||||
|
|
||||||
for (const e of rawEdits) {
|
for (const e of rawEdits) {
|
||||||
if (!e || typeof e !== 'object' || Array.isArray(e)) continue;
|
if (!e || typeof e !== 'object' || Array.isArray(e)) continue;
|
||||||
|
|
||||||
const obj = e as Record<string, unknown>;
|
const obj = e as Record<string, unknown>;
|
||||||
const oldText = typeof obj.old_text === 'string' ? obj.old_text : '';
|
const oldText = typeof obj.old_text === 'string' ? obj.old_text : '';
|
||||||
|
|
||||||
if (!oldText) continue;
|
if (!oldText) continue;
|
||||||
|
|
||||||
const newText = typeof obj.new_text === 'string' ? obj.new_text : '';
|
const newText = typeof obj.new_text === 'string' ? obj.new_text : '';
|
||||||
edits.push({ oldText, newText });
|
|
||||||
|
edits.push({ newText, oldText });
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultObj = tryParseToolResultObject(section.toolResult);
|
const resultObj = tryParseToolResultObject(section.toolResult);
|
||||||
|
|
||||||
let resultMessage: string | undefined;
|
let resultMessage: string | undefined;
|
||||||
let editsApplied: number | undefined;
|
let editsApplied: number | undefined;
|
||||||
let errorMessage: string | undefined;
|
let errorMessage: string | undefined;
|
||||||
|
|
||||||
if (typeof resultObj?.error === 'string') {
|
if (typeof resultObj?.error === 'string') {
|
||||||
errorMessage = resultObj.error;
|
errorMessage = resultObj.error;
|
||||||
} else if (resultObj) {
|
} else if (resultObj) {
|
||||||
if (typeof resultObj.result === 'string') {
|
if (typeof resultObj.result === 'string') {
|
||||||
resultMessage = resultObj.result;
|
resultMessage = resultObj.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isFinite(Number(resultObj.edits_applied))) {
|
if (Number.isFinite(Number(resultObj.edits_applied))) {
|
||||||
editsApplied = Number(resultObj.edits_applied);
|
editsApplied = Number(resultObj.edits_applied);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
edits,
|
||||||
|
editsApplied,
|
||||||
|
errorMessage,
|
||||||
fileName,
|
fileName,
|
||||||
filePath: rawPath,
|
filePath: rawPath,
|
||||||
edits,
|
resultMessage
|
||||||
resultMessage,
|
|
||||||
editsApplied,
|
|
||||||
errorMessage
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -5,9 +5,9 @@
|
|||||||
// file only deals with what's strictly about *calling* the tool, since
|
// file only deals with what's strictly about *calling* the tool, since
|
||||||
// the error / exit status elide from call-section to result-section.
|
// the error / exit status elide from call-section to result-section.
|
||||||
|
|
||||||
|
import { parseToolArgs } from './_shared';
|
||||||
import { BuiltInTool } from '$lib/enums';
|
import { BuiltInTool } from '$lib/enums';
|
||||||
import type { AgenticSection } from '$lib/utils';
|
import type { AgenticSection } from '$lib/utils';
|
||||||
import { parseToolArgs } from './_shared';
|
|
||||||
|
|
||||||
export type ExecShellCommandMeta = {
|
export type ExecShellCommandMeta = {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -15,9 +15,12 @@ export type ExecShellCommandMeta = {
|
|||||||
|
|
||||||
export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
|
export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.EXEC_SHELL_COMMAND, section);
|
const args = parseToolArgs(BuiltInTool.EXEC_SHELL_COMMAND, section);
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
const commandRaw = args.command ?? args.cmd ?? args.shell_command;
|
const commandRaw = args.command ?? args.cmd ?? args.shell_command;
|
||||||
|
|
||||||
if (typeof commandRaw !== 'string' || !commandRaw) return null;
|
if (typeof commandRaw !== 'string' || !commandRaw) return null;
|
||||||
|
|
||||||
return { command: commandRaw };
|
return { command: commandRaw };
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -4,9 +4,9 @@
|
|||||||
// parser keeps the original raw-text fallback for MCP servers that
|
// parser keeps the original raw-text fallback for MCP servers that
|
||||||
// emit unparseable output.
|
// emit unparseable output.
|
||||||
|
|
||||||
import { BuiltInTool } from '$lib/enums';
|
|
||||||
import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
|
|
||||||
import { parseToolArgs } from './_shared';
|
import { parseToolArgs } from './_shared';
|
||||||
|
import { BuiltInTool } from '$lib/enums';
|
||||||
|
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
|
||||||
|
|
||||||
export type FileGlobSearchMeta = {
|
export type FileGlobSearchMeta = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -19,11 +19,13 @@ export type FileGlobSearchMeta = {
|
|||||||
|
|
||||||
export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearchMeta | null {
|
export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearchMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.FILE_GLOB_SEARCH, section);
|
const args = parseToolArgs(BuiltInTool.FILE_GLOB_SEARCH, section);
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
const path = typeof args.path === 'string' ? args.path : '';
|
const path = typeof args.path === 'string' ? args.path : '';
|
||||||
const include = typeof args.include === 'string' && args.include ? args.include : '**';
|
const include = typeof args.include === 'string' && args.include ? args.include : '**';
|
||||||
const exclude = typeof args.exclude === 'string' && args.exclude ? args.exclude : undefined;
|
const exclude = typeof args.exclude === 'string' && args.exclude ? args.exclude : undefined;
|
||||||
|
|
||||||
if (!path) return null;
|
if (!path) return null;
|
||||||
|
|
||||||
let matches: string[] = [];
|
let matches: string[] = [];
|
||||||
@@ -31,17 +33,21 @@ export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearch
|
|||||||
let errorMessage: string | undefined;
|
let errorMessage: string | undefined;
|
||||||
|
|
||||||
const toolResultString = section.toolResult;
|
const toolResultString = section.toolResult;
|
||||||
|
|
||||||
if (toolResultString) {
|
if (toolResultString) {
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(toolResultString);
|
const parsed: unknown = JSON.parse(toolResultString);
|
||||||
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
const obj = parsed as Record<string, unknown>;
|
const obj = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
if (typeof obj.error === 'string') {
|
if (typeof obj.error === 'string') {
|
||||||
errorMessage = obj.error;
|
errorMessage = obj.error;
|
||||||
} else if (typeof obj.plain_text_response === 'string') {
|
} else if (typeof obj.plain_text_response === 'string') {
|
||||||
const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
|
const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
|
||||||
totalMatches = total;
|
totalMatches = total;
|
||||||
});
|
});
|
||||||
|
|
||||||
matches = split.lines;
|
matches = split.lines;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,9 +56,10 @@ export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearch
|
|||||||
const split = splitSearchSummaryList(toolResultString, (total) => {
|
const split = splitSearchSummaryList(toolResultString, (total) => {
|
||||||
totalMatches = total;
|
totalMatches = total;
|
||||||
});
|
});
|
||||||
|
|
||||||
matches = split.lines;
|
matches = split.lines;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { path, include, exclude, matches, totalMatches, errorMessage };
|
return { errorMessage, exclude, include, matches, path, totalMatches };
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-12
@@ -5,9 +5,9 @@
|
|||||||
// fallback so MCP servers that return unparseable output still get
|
// fallback so MCP servers that return unparseable output still get
|
||||||
// surfaced.
|
// surfaced.
|
||||||
|
|
||||||
import { BuiltInTool } from '$lib/enums';
|
|
||||||
import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
|
|
||||||
import { parseToolArgs } from './_shared';
|
import { parseToolArgs } from './_shared';
|
||||||
|
import { BuiltInTool } from '$lib/enums';
|
||||||
|
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
|
||||||
|
|
||||||
export type GrepSearchMatch = {
|
export type GrepSearchMatch = {
|
||||||
file: string;
|
file: string;
|
||||||
@@ -28,10 +28,12 @@ export type GrepSearchMeta = {
|
|||||||
|
|
||||||
export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
|
export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
|
const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
const path = typeof args.path === 'string' ? args.path : '';
|
const path = typeof args.path === 'string' ? args.path : '';
|
||||||
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
||||||
|
|
||||||
if (!path || !pattern) return null;
|
if (!path || !pattern) return null;
|
||||||
|
|
||||||
const include = typeof args.include === 'string' && args.include ? args.include : '**';
|
const include = typeof args.include === 'string' && args.include ? args.include : '**';
|
||||||
@@ -43,17 +45,21 @@ export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | n
|
|||||||
let errorMessage: string | undefined;
|
let errorMessage: string | undefined;
|
||||||
|
|
||||||
const toolResultString = section.toolResult;
|
const toolResultString = section.toolResult;
|
||||||
|
|
||||||
if (toolResultString) {
|
if (toolResultString) {
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(toolResultString);
|
const parsed: unknown = JSON.parse(toolResultString);
|
||||||
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
const obj = parsed as Record<string, unknown>;
|
const obj = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
if (typeof obj.error === 'string') {
|
if (typeof obj.error === 'string') {
|
||||||
errorMessage = obj.error;
|
errorMessage = obj.error;
|
||||||
} else if (typeof obj.plain_text_response === 'string') {
|
} else if (typeof obj.plain_text_response === 'string') {
|
||||||
const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
|
const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
|
||||||
totalMatches = total;
|
totalMatches = total;
|
||||||
});
|
});
|
||||||
|
|
||||||
matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
|
matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,19 +70,20 @@ export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | n
|
|||||||
const split = splitSearchSummaryList(toolResultString, (total) => {
|
const split = splitSearchSummaryList(toolResultString, (total) => {
|
||||||
totalMatches = total;
|
totalMatches = total;
|
||||||
});
|
});
|
||||||
|
|
||||||
matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
|
matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
errorMessage,
|
||||||
|
exclude,
|
||||||
|
include,
|
||||||
|
matches,
|
||||||
path,
|
path,
|
||||||
pattern,
|
pattern,
|
||||||
include,
|
|
||||||
exclude,
|
|
||||||
showLineNumbers,
|
showLineNumbers,
|
||||||
matches,
|
totalMatches
|
||||||
totalMatches,
|
|
||||||
errorMessage
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,24 +92,29 @@ function parseGrepLine(line: string, showLineNumbers: boolean): GrepSearchMatch
|
|||||||
// <file>:<content> when return_line_numbers=false
|
// <file>:<content> when return_line_numbers=false
|
||||||
// <file>:<lineno>:<content> when return_line_numbers=true
|
// <file>:<lineno>:<content> when return_line_numbers=true
|
||||||
const firstColon = line.indexOf(':');
|
const firstColon = line.indexOf(':');
|
||||||
|
|
||||||
if (firstColon === -1) {
|
if (firstColon === -1) {
|
||||||
return { file: line, content: '' };
|
return { content: '', file: line };
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = line.slice(0, firstColon);
|
const file = line.slice(0, firstColon);
|
||||||
const tail = line.slice(firstColon + 1);
|
const tail = line.slice(firstColon + 1);
|
||||||
|
|
||||||
if (!showLineNumbers) {
|
if (!showLineNumbers) {
|
||||||
return { file, content: tail };
|
return { content: tail, file };
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondColon = tail.indexOf(':');
|
const secondColon = tail.indexOf(':');
|
||||||
|
|
||||||
if (secondColon === -1) {
|
if (secondColon === -1) {
|
||||||
return { file, content: tail };
|
return { content: tail, file };
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineNum = parseInt(tail.slice(0, secondColon), 10);
|
const lineNum = parseInt(tail.slice(0, secondColon), 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
content: tail.slice(secondColon + 1),
|
||||||
file,
|
file,
|
||||||
line: Number.isFinite(lineNum) ? lineNum : undefined,
|
line: Number.isFinite(lineNum) ? lineNum : undefined
|
||||||
content: tail.slice(secondColon + 1)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -3,14 +3,14 @@
|
|||||||
// `start_line`+`line_count`). Args are parsed partially so a header
|
// `start_line`+`line_count`). Args are parsed partially so a header
|
||||||
// can render incrementally as the file path streams in.
|
// can render incrementally as the file path streams in.
|
||||||
|
|
||||||
import { BuiltInTool } from '$lib/enums';
|
import { parseToolArgs } from './_shared';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
FILE_PATH_SEPARATOR_REGEX,
|
FILE_PATH_SEPARATOR_REGEX,
|
||||||
TEXT_LANGUAGE_PREFIX_REGEX
|
TEXT_LANGUAGE_PREFIX_REGEX
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import { getFileTypeByExtension, type AgenticSection } from '$lib/utils';
|
import { BuiltInTool } from '$lib/enums';
|
||||||
import { parseToolArgs } from './_shared';
|
import { type AgenticSection, getFileTypeByExtension } from '$lib/utils';
|
||||||
|
|
||||||
export type ReadFileMeta = {
|
export type ReadFileMeta = {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@@ -20,13 +20,14 @@ export type ReadFileMeta = {
|
|||||||
|
|
||||||
export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null {
|
export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.READ_FILE, section, { partial: true });
|
const args = parseToolArgs(BuiltInTool.READ_FILE, section, { partial: true });
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
||||||
|
|
||||||
if (typeof rawPath !== 'string' || !rawPath) return null;
|
if (typeof rawPath !== 'string' || !rawPath) return null;
|
||||||
|
|
||||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||||
|
|
||||||
// Models emit range arguments under several aliases. Accept all to
|
// Models emit range arguments under several aliases. Accept all to
|
||||||
// stay forgiving across prompt variations.
|
// stay forgiving across prompt variations.
|
||||||
const startRaw = args.start_line ?? args.line_start ?? args.startLine ?? args.from_line;
|
const startRaw = args.start_line ?? args.line_start ?? args.startLine ?? args.from_line;
|
||||||
@@ -34,19 +35,22 @@ export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null
|
|||||||
const countRaw = args.line_count ?? args.count ?? args.num_lines;
|
const countRaw = args.line_count ?? args.count ?? args.num_lines;
|
||||||
|
|
||||||
let lineRange: { start: number; end: number } | null = null;
|
let lineRange: { start: number; end: number } | null = null;
|
||||||
|
|
||||||
const sNum = Number(startRaw);
|
const sNum = Number(startRaw);
|
||||||
const eNum = Number(endRaw);
|
const eNum = Number(endRaw);
|
||||||
|
|
||||||
if (startRaw != null && endRaw != null && Number.isFinite(sNum) && Number.isFinite(eNum)) {
|
if (startRaw != null && endRaw != null && Number.isFinite(sNum) && Number.isFinite(eNum)) {
|
||||||
lineRange = { start: sNum, end: eNum };
|
lineRange = { end: eNum, start: sNum };
|
||||||
} else if (startRaw != null && countRaw != null) {
|
} else if (startRaw != null && countRaw != null) {
|
||||||
const cNum = Number(countRaw);
|
const cNum = Number(countRaw);
|
||||||
|
|
||||||
if (Number.isFinite(sNum) && Number.isFinite(cNum)) {
|
if (Number.isFinite(sNum) && Number.isFinite(cNum)) {
|
||||||
lineRange = { start: sNum, end: sNum + cNum - 1 };
|
lineRange = { end: sNum + cNum - 1, start: sNum };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileType = getFileTypeByExtension(fileName);
|
const fileType = getFileTypeByExtension(fileName);
|
||||||
const language = fileType ? fileType.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') : DEFAULT_LANGUAGE;
|
const language = fileType ? fileType.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') : DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
return { fileName, lineRange, language };
|
return { fileName, language, lineRange };
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -5,9 +5,9 @@
|
|||||||
// failure renders as a flat line beginning with `Error:`. Both shapes
|
// failure renders as a flat line beginning with `Error:`. Both shapes
|
||||||
// are handled.
|
// are handled.
|
||||||
|
|
||||||
|
import { parseToolArgs } from './_shared';
|
||||||
import { BuiltInTool } from '$lib/enums';
|
import { BuiltInTool } from '$lib/enums';
|
||||||
import type { AgenticSection } from '$lib/utils';
|
import type { AgenticSection } from '$lib/utils';
|
||||||
import { parseToolArgs } from './_shared';
|
|
||||||
|
|
||||||
export type RunJavascriptMeta = {
|
export type RunJavascriptMeta = {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -17,30 +17,37 @@ export type RunJavascriptMeta = {
|
|||||||
|
|
||||||
export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMeta | null {
|
export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.RUN_JAVASCRIPT, section);
|
const args = parseToolArgs(BuiltInTool.RUN_JAVASCRIPT, section);
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
const code = typeof args.code === 'string' ? args.code : '';
|
const code = typeof args.code === 'string' ? args.code : '';
|
||||||
|
|
||||||
if (!code) return null;
|
if (!code) return null;
|
||||||
|
|
||||||
const timeoutRaw = Number(args.timeout_ms);
|
const timeoutRaw = Number(args.timeout_ms);
|
||||||
const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : undefined;
|
const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : undefined;
|
||||||
|
|
||||||
let errorMessage: string | undefined;
|
let errorMessage: string | undefined;
|
||||||
|
|
||||||
const toolResultString = section.toolResult;
|
const toolResultString = section.toolResult;
|
||||||
|
|
||||||
if (toolResultString) {
|
if (toolResultString) {
|
||||||
// Branches matter here: a JSON object can carry `error`, but a
|
// Branches matter here: a JSON object can carry `error`, but a
|
||||||
// JSON array always represents successful output (sandbox returns
|
// JSON array always represents successful output (sandbox returns
|
||||||
// the array of values). Only when the result isn't a JSON object
|
// the array of values). Only when the result isn't a JSON object
|
||||||
// do we scan raw lines for the `Error:` prefix.
|
// do we scan raw lines for the `Error:` prefix.
|
||||||
let parsedObject: Record<string, unknown> | null = null;
|
let parsedObject: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(toolResultString);
|
const parsed: unknown = JSON.parse(toolResultString);
|
||||||
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
parsedObject = parsed as Record<string, unknown>;
|
parsedObject = parsed as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
parsedObject = null;
|
parsedObject = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof parsedObject?.error === 'string') {
|
if (typeof parsedObject?.error === 'string') {
|
||||||
errorMessage = parsedObject.error;
|
errorMessage = parsedObject.error;
|
||||||
} else if (!parsedObject) {
|
} else if (!parsedObject) {
|
||||||
@@ -48,9 +55,10 @@ export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMe
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.find((line) => line.startsWith('Error:'));
|
.find((line) => line.startsWith('Error:'));
|
||||||
|
|
||||||
if (errorLine) errorMessage = errorLine.slice('Error:'.length).trim();
|
if (errorLine) errorMessage = errorLine.slice('Error:'.length).trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { code, timeoutMs, errorMessage };
|
return { code, errorMessage, timeoutMs };
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-8
@@ -3,14 +3,14 @@
|
|||||||
// finishes) and surfaces `bytes`, `result`, and `error` from the
|
// finishes) and surfaces `bytes`, `result`, and `error` from the
|
||||||
// result blob.
|
// result blob.
|
||||||
|
|
||||||
import { BuiltInTool } from '$lib/enums';
|
import { parseToolArgs } from './_shared';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
FILE_PATH_SEPARATOR_REGEX,
|
FILE_PATH_SEPARATOR_REGEX,
|
||||||
TEXT_LANGUAGE_PREFIX_REGEX
|
TEXT_LANGUAGE_PREFIX_REGEX
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import { getFileTypeByExtension, tryParseToolResultObject, type AgenticSection } from '$lib/utils';
|
import { BuiltInTool } from '$lib/enums';
|
||||||
import { parseToolArgs } from './_shared';
|
import { type AgenticSection, getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
|
||||||
|
|
||||||
export type WriteFileMeta = {
|
export type WriteFileMeta = {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@@ -24,18 +24,19 @@ export type WriteFileMeta = {
|
|||||||
|
|
||||||
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
|
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
|
||||||
const args = parseToolArgs(BuiltInTool.WRITE_FILE, section, { partial: true });
|
const args = parseToolArgs(BuiltInTool.WRITE_FILE, section, { partial: true });
|
||||||
|
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
|
|
||||||
// Tool contracts drifted over time: some models emit `path`,
|
// Tool contracts drifted over time: some models emit `path`,
|
||||||
// others `file_path` / `filePath`. Accept all three.
|
// others `file_path` / `filePath`. Accept all three.
|
||||||
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
||||||
|
|
||||||
if (typeof rawPath !== 'string' || !rawPath) return null;
|
if (typeof rawPath !== 'string' || !rawPath) return null;
|
||||||
|
|
||||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||||
const content = typeof args.content === 'string' ? args.content : '';
|
const content = typeof args.content === 'string' ? args.content : '';
|
||||||
const language =
|
const language =
|
||||||
getFileTypeByExtension(rawPath)?.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') ?? DEFAULT_LANGUAGE;
|
getFileTypeByExtension(rawPath)?.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') ?? DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
const resultObj = tryParseToolResultObject(section.toolResult);
|
const resultObj = tryParseToolResultObject(section.toolResult);
|
||||||
const bytesWritten =
|
const bytesWritten =
|
||||||
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
|
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
|
||||||
@@ -43,12 +44,12 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
|
|||||||
const errorMessage = typeof resultObj?.error === 'string' ? resultObj.error : undefined;
|
const errorMessage = typeof resultObj?.error === 'string' ? resultObj.error : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
bytesWritten,
|
||||||
|
content,
|
||||||
|
errorMessage,
|
||||||
fileName,
|
fileName,
|
||||||
filePath: rawPath,
|
filePath: rawPath,
|
||||||
language,
|
language,
|
||||||
content,
|
resultMessage
|
||||||
bytesWritten,
|
|
||||||
resultMessage,
|
|
||||||
errorMessage
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -6,9 +6,9 @@
|
|||||||
ChatMessageUserBubble
|
ChatMessageUserBubble
|
||||||
} from '$lib/components/app/chat';
|
} from '$lib/components/app/chat';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getMessageEditContext } from '$lib/contexts';
|
||||||
|
import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
|
||||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||||
import { isLoading } from '$lib/stores/chat.svelte';
|
import { isLoading } from '$lib/stores/chat.svelte';
|
||||||
import { MessageRole, ChatMessageStatisticsMode } from '$lib/enums';
|
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -35,19 +35,19 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
message,
|
|
||||||
siblingInfo = null,
|
|
||||||
deletionInfo,
|
deletionInfo,
|
||||||
isLastUserMessage = false,
|
isLastUserMessage = false,
|
||||||
|
message,
|
||||||
nextAssistantMessage = null,
|
nextAssistantMessage = null,
|
||||||
showDeleteDialog,
|
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
|
onCopy,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
onForkConversation,
|
onForkConversation,
|
||||||
onShowDeleteDialogChange,
|
|
||||||
onNavigateToSibling,
|
onNavigateToSibling,
|
||||||
onCopy
|
onShowDeleteDialogChange,
|
||||||
|
showDeleteDialog,
|
||||||
|
siblingInfo = null
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
@@ -60,13 +60,14 @@
|
|||||||
// For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
|
// For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
|
||||||
let storedReadingStats = $derived.by(() => {
|
let storedReadingStats = $derived.by(() => {
|
||||||
const timings = nextAssistantMessage?.timings;
|
const timings = nextAssistantMessage?.timings;
|
||||||
|
|
||||||
if (!timings?.prompt_n || !timings?.prompt_ms) return null;
|
if (!timings?.prompt_n || !timings?.prompt_ms) return null;
|
||||||
|
|
||||||
const agentic = timings.agentic;
|
const agentic = timings.agentic;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n,
|
promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms,
|
||||||
promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms
|
promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Card } from '$lib/components/ui/card';
|
|
||||||
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
|
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
|
||||||
|
import { Card } from '$lib/components/ui/card';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import type { DatabaseMessageExtra } from '$lib/types/database';
|
import type { DatabaseMessageExtra } from '$lib/types/database';
|
||||||
|
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
content,
|
|
||||||
attachments = [],
|
attachments = [],
|
||||||
renderMarkdown = false,
|
|
||||||
textColorClass = 'text-foreground',
|
|
||||||
cardBgClass = 'dark:bg-primary/15',
|
cardBgClass = 'dark:bg-primary/15',
|
||||||
maxHeightStyle = ''
|
content,
|
||||||
|
maxHeightStyle = '',
|
||||||
|
renderMarkdown = false,
|
||||||
|
textColorClass = 'text-foreground'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let isMultiline = $state(false);
|
let isMultiline = $state(false);
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
if (content.includes('\n')) {
|
if (content.includes('\n')) {
|
||||||
isMultiline = true;
|
isMultiline = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
|
|
||||||
import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
|
import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
|
||||||
|
import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
|
||||||
import { useMessageEditContext } from '$lib/hooks/use-message-edit-context.svelte';
|
import { useMessageEditContext } from '$lib/hooks/use-message-edit-context.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
class: className = '',
|
class: className = '',
|
||||||
content,
|
content,
|
||||||
extras = [],
|
extras = [],
|
||||||
onSendImmediately,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onSendImmediately
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const editCtx = useMessageEditContext({
|
const editCtx = useMessageEditContext({
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||||
import type { Snippet, Component } from 'svelte';
|
import type { Component, Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
icon: Component<{ class?: string }>;
|
icon: Component<{ class?: string }>;
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
actions: Snippet;
|
actions: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { icon: IconComponent, message, actions }: Props = $props();
|
let { actions, icon: IconComponent, message }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="my-2 rounded-lg border border-border bg-card p-3">
|
<div class="my-2 rounded-lg border border-border bg-card p-3">
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ChatMessageActionCard from './ChatMessageActionCard.svelte';
|
||||||
import { RotateCw } from '@lucide/svelte';
|
import { RotateCw } from '@lucide/svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import ChatMessageActionCard from './ChatMessageActionCard.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onDecision: (shouldContinue: boolean) => void;
|
onDecision: (shouldContinue: boolean) => void;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user