Compare commits
11 Commits
v0.11.1-de
...
v0.11.2-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6de6ef5a4a | ||
|
|
4dee154490 | ||
|
|
ef388adc4f | ||
|
|
e8cfad1266 | ||
|
|
3f82dd21fe | ||
|
|
dc13d9a7d0 | ||
|
|
29557fba6d | ||
|
|
dea5079713 | ||
|
|
ddc58a2c3c | ||
|
|
eafd4d83af | ||
|
|
1a0734c6b1 |
@@ -123,3 +123,6 @@ To build the Desktop App from source:
|
|||||||
1. Clone the repo.
|
1. Clone the repo.
|
||||||
2. Run `npm install` (requires pnpm or npm 7+ for workspaces).
|
2. Run `npm install` (requires pnpm or npm 7+ for workspaces).
|
||||||
3. Run `npm run build --workspace @neuralnomads/codenomad-electron-app`.
|
3. Run `npm run build --workspace @neuralnomads/codenomad-electron-app`.
|
||||||
|
|
||||||
|
[](https://star-history.com/#NeuralNomadsAI/CodeNomad&Date)
|
||||||
|
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -2809,9 +2809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@opencode-ai/sdk": {
|
"node_modules/@opencode-ai/sdk": {
|
||||||
"version": "1.1.11",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.6.tgz",
|
||||||
"integrity": "sha512-vqdNDz8Q+4bygmDdQem6oxhU31ci4JVdoND4ZJNeCs9x6OIU6MM3ybgemGpzNkgtJDlfb4xCdrPaZZ6Sr3V1IQ==",
|
"integrity": "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@pinojs/redact": {
|
"node_modules/@pinojs/redact": {
|
||||||
@@ -11985,7 +11985,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12021,7 +12021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12062,7 +12062,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12070,12 +12070,12 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@opencode-ai/sdk": "1.1.11",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
"@solidjs/router": "^0.13.0",
|
"@solidjs/router": "^0.13.0",
|
||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
showError(message || `Login failed (${res.status})`)
|
showError(message || `Login failed (${res.status})`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.location.href = "/"
|
// Replace history entry so Back doesn't return to /login.
|
||||||
|
window.location.replace("/")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e && e.message ? e.message : String(e))
|
showError(e && e.message ? e.message : String(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,19 @@ function getTokenHtml(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
app.get("/login", async (_request, reply) => {
|
app.get("/login", async (request, reply) => {
|
||||||
|
// If already authenticated, don't show the login page.
|
||||||
|
const session = deps.authManager.getSessionFromRequest(request)
|
||||||
|
if (session) {
|
||||||
|
reply.redirect("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid caching the login page (helps with bfcache/back behavior).
|
||||||
|
reply.header("Cache-Control", "no-store")
|
||||||
|
reply.header("Pragma", "no-cache")
|
||||||
|
reply.header("Expires", "0")
|
||||||
|
|
||||||
const status = deps.authManager.getStatus()
|
const status = deps.authManager.getStatus()
|
||||||
reply.type("text/html").send(getLoginHtml(status.username))
|
reply.type("text/html").send(getLoginHtml(status.username))
|
||||||
})
|
})
|
||||||
@@ -67,6 +79,11 @@ export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid caching the token bootstrap page.
|
||||||
|
reply.header("Cache-Control", "no-store")
|
||||||
|
reply.header("Pragma", "no-cache")
|
||||||
|
reply.header("Expires", "0")
|
||||||
|
|
||||||
reply.type("text/html").send(getTokenHtml())
|
reply.type("text/html").send(getTokenHtml())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@opencode-ai/sdk": "1.1.11",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
"@solidjs/router": "^0.13.0",
|
"@solidjs/router": "^0.13.0",
|
||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
||||||
import { Dialog } from "@kobalte/core/dialog"
|
import { Dialog } from "@kobalte/core/dialog"
|
||||||
import { Toaster } from "solid-toast"
|
import { Toaster } from "solid-toast"
|
||||||
|
import useMediaQuery from "@suid/material/useMediaQuery"
|
||||||
|
import { Minimize2 } from "lucide-solid"
|
||||||
import AlertDialog from "./components/alert-dialog"
|
import AlertDialog from "./components/alert-dialog"
|
||||||
import FolderSelectionView from "./components/folder-selection-view"
|
import FolderSelectionView from "./components/folder-selection-view"
|
||||||
import { showConfirmDialog } from "./stores/alerts"
|
import { showConfirmDialog } from "./stores/alerts"
|
||||||
@@ -82,6 +84,46 @@ const App: Component = () => {
|
|||||||
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
||||||
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
||||||
|
|
||||||
|
const phoneQuery = useMediaQuery("(max-width: 767px)")
|
||||||
|
const isPhoneLayout = createMemo(() => phoneQuery())
|
||||||
|
|
||||||
|
// In-memory only: hides chrome on phone; may also request browser fullscreen.
|
||||||
|
const [mobileFullscreenMode, setMobileFullscreenMode] = createSignal(false)
|
||||||
|
const [browserFullscreenActive, setBrowserFullscreenActive] = createSignal(false)
|
||||||
|
|
||||||
|
const fullscreenSupported = () => {
|
||||||
|
if (typeof document === "undefined") return false
|
||||||
|
const el = document.documentElement as any
|
||||||
|
return Boolean(document.fullscreenEnabled) && typeof el?.requestFullscreen === "function"
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncBrowserFullscreenState = () => {
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
setBrowserFullscreenActive(Boolean(document.fullscreenElement))
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterMobileFullscreen = async () => {
|
||||||
|
if (!isPhoneLayout()) return
|
||||||
|
setMobileFullscreenMode(true)
|
||||||
|
if (!fullscreenSupported()) return
|
||||||
|
try {
|
||||||
|
await document.documentElement.requestFullscreen()
|
||||||
|
} catch {
|
||||||
|
// Ignore: immersive mode still works without browser fullscreen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitMobileFullscreen = async () => {
|
||||||
|
if (typeof document !== "undefined" && document.fullscreenElement && typeof document.exitFullscreen === "function") {
|
||||||
|
try {
|
||||||
|
await document.exitFullscreen()
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMobileFullscreenMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const shouldShow =
|
const shouldShow =
|
||||||
@@ -95,6 +137,56 @@ const App: Component = () => {
|
|||||||
setInstanceTabBarHeight(element?.offsetHeight ?? 0)
|
setInstanceTabBarHeight(element?.offsetHeight ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
syncBrowserFullscreenState()
|
||||||
|
document.addEventListener("fullscreenchange", syncBrowserFullscreenState)
|
||||||
|
onCleanup(() => document.removeEventListener("fullscreenchange", syncBrowserFullscreenState))
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
const vv = window.visualViewport
|
||||||
|
if (!vv) return
|
||||||
|
|
||||||
|
const updateKeyboardOffset = () => {
|
||||||
|
// visualViewport shrinks when the OSK is visible. Use the delta as a bottom inset.
|
||||||
|
const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop)
|
||||||
|
document.documentElement.style.setProperty("--keyboard-offset", `${Math.floor(inset)}px`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = () => requestAnimationFrame(updateKeyboardOffset)
|
||||||
|
schedule()
|
||||||
|
vv.addEventListener("resize", schedule)
|
||||||
|
vv.addEventListener("scroll", schedule)
|
||||||
|
window.addEventListener("orientationchange", schedule)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
vv.removeEventListener("resize", schedule)
|
||||||
|
vv.removeEventListener("scroll", schedule)
|
||||||
|
window.removeEventListener("orientationchange", schedule)
|
||||||
|
document.documentElement.style.removeProperty("--keyboard-offset")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the user exits browser fullscreen via browser UI, restore chrome.
|
||||||
|
let lastBrowserFullscreen = false
|
||||||
|
createEffect(() => {
|
||||||
|
const active = browserFullscreenActive()
|
||||||
|
const mode = mobileFullscreenMode()
|
||||||
|
if (mode && lastBrowserFullscreen && !active) {
|
||||||
|
setMobileFullscreenMode(false)
|
||||||
|
}
|
||||||
|
lastBrowserFullscreen = active
|
||||||
|
})
|
||||||
|
|
||||||
|
// If we leave phone layout (rotation / resize), restore chrome.
|
||||||
|
createEffect(() => {
|
||||||
|
if (!isPhoneLayout() && mobileFullscreenMode()) {
|
||||||
|
void exitMobileFullscreen()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
|
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
|
||||||
})
|
})
|
||||||
@@ -405,19 +497,34 @@ const App: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div class="h-screen w-screen flex flex-col">
|
<div class="h-screen w-screen flex flex-col" style={{ height: "100dvh", "padding-bottom": "var(--keyboard-offset, 0px)" }}>
|
||||||
|
<Show when={isPhoneLayout() && mobileFullscreenMode()}>
|
||||||
|
<div class="mobile-fullscreen-exit-wrapper">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-scroll-button mobile-fullscreen-exit-button"
|
||||||
|
onClick={() => void exitMobileFullscreen()}
|
||||||
|
aria-label={t("instanceShell.fullscreen.exit")}
|
||||||
|
title={t("instanceShell.fullscreen.exit")}
|
||||||
|
>
|
||||||
|
<Minimize2 class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={!hasInstances()}
|
when={!hasInstances()}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<InstanceTabs
|
<Show when={!isPhoneLayout() || !mobileFullscreenMode()}>
|
||||||
instances={instances()}
|
<InstanceTabs
|
||||||
activeInstanceId={activeInstanceId()}
|
instances={instances()}
|
||||||
onSelect={setActiveInstanceId}
|
activeInstanceId={activeInstanceId()}
|
||||||
onClose={handleCloseInstance}
|
onSelect={setActiveInstanceId}
|
||||||
onNew={handleNewInstanceRequest}
|
onClose={handleCloseInstance}
|
||||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
onNew={handleNewInstanceRequest}
|
||||||
/>
|
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<For each={Array.from(instances().values())}>
|
<For each={Array.from(instances().values())}>
|
||||||
{(instance) => {
|
{(instance) => {
|
||||||
@@ -435,7 +542,10 @@ const App: Component = () => {
|
|||||||
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
||||||
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
||||||
onExecuteCommand={executeCommand}
|
onExecuteCommand={executeCommand}
|
||||||
tabBarOffset={instanceTabBarHeight()}
|
tabBarOffset={isPhoneLayout() && mobileFullscreenMode() ? 0 : instanceTabBarHeight()}
|
||||||
|
mobileFullscreenMode={isPhoneLayout() && mobileFullscreenMode()}
|
||||||
|
onEnterMobileFullscreen={() => void enterMobileFullscreen()}
|
||||||
|
onExitMobileFullscreen={() => void exitMobileFullscreen()}
|
||||||
/>
|
/>
|
||||||
</InstanceMetadataProvider>
|
</InstanceMetadataProvider>
|
||||||
|
|
||||||
|
|||||||
123
packages/ui/src/components/context-meter.tsx
Normal file
123
packages/ui/src/components/context-meter.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { Component } from "solid-js"
|
||||||
|
|
||||||
|
interface ContextMeterProps {
|
||||||
|
usedTokens: number
|
||||||
|
availableTokens: number | null
|
||||||
|
formatTokens: (value: number) => string
|
||||||
|
usedLabel: string
|
||||||
|
availableLabel: string
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFillColor(percent: number): string {
|
||||||
|
if (percent >= 0.8) return "var(--status-error)"
|
||||||
|
if (percent >= 0.6) return "var(--status-warning)"
|
||||||
|
return "var(--status-success)"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMeter: Component<ContextMeterProps> = (props) => {
|
||||||
|
const hasAvailable = () => typeof props.availableTokens === "number" && props.availableTokens > 0
|
||||||
|
const used = () => (typeof props.usedTokens === "number" && props.usedTokens > 0 ? props.usedTokens : 0)
|
||||||
|
const available = () => (hasAvailable() ? (props.availableTokens as number) : null)
|
||||||
|
|
||||||
|
const percent = () => {
|
||||||
|
const usedValue = used()
|
||||||
|
const availableValue = available()
|
||||||
|
if (availableValue === null || availableValue <= 0) return null
|
||||||
|
|
||||||
|
// Heuristic: if available >= used, treat it like a capacity/limit.
|
||||||
|
// Otherwise treat it like remaining tokens.
|
||||||
|
const ratio = availableValue >= usedValue ? usedValue / availableValue : usedValue / (usedValue + availableValue)
|
||||||
|
return clamp(ratio, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillColor = () => {
|
||||||
|
const value = percent()
|
||||||
|
if (value === null) return "var(--border-base)"
|
||||||
|
return resolveFillColor(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentLabel = () => {
|
||||||
|
const value = percent()
|
||||||
|
if (value === null) return "--"
|
||||||
|
return `${Math.round(value * 100)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerClass =
|
||||||
|
`inline-flex items-center gap-2 rounded-full border border-base px-2 py-0.5 text-xs text-primary ${props.class ?? ""}`
|
||||||
|
|
||||||
|
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
|
||||||
|
const rad = (angleDeg * Math.PI) / 180
|
||||||
|
return {
|
||||||
|
x: cx + r * Math.cos(rad),
|
||||||
|
y: cy + r * Math.sin(rad),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeSectorPath(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {
|
||||||
|
const start = polarToCartesian(cx, cy, r, startAngle)
|
||||||
|
const end = polarToCartesian(cx, cy, r, endAngle)
|
||||||
|
const delta = ((endAngle - startAngle) % 360 + 360) % 360
|
||||||
|
const largeArc = delta > 180 ? 1 : 0
|
||||||
|
|
||||||
|
return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y} Z`
|
||||||
|
}
|
||||||
|
|
||||||
|
const circle = () => {
|
||||||
|
const value = percent()
|
||||||
|
const size = 22
|
||||||
|
const r = 9
|
||||||
|
const cx = 11
|
||||||
|
const cy = 11
|
||||||
|
const progress = value === null ? 0 : value
|
||||||
|
const startAngle = -90
|
||||||
|
const endAngle = startAngle + progress * 360
|
||||||
|
const isFull = progress >= 0.999
|
||||||
|
const hasFill = progress > 0.001
|
||||||
|
|
||||||
|
const sectorPath = hasFill && !isFull ? describeSectorPath(cx, cy, r, startAngle, endAngle) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 22 22"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ flex: "0 0 auto" }}
|
||||||
|
>
|
||||||
|
<circle cx={String(cx)} cy={String(cy)} r={String(r)} fill="var(--surface-secondary)" />
|
||||||
|
<circle cx={String(cx)} cy={String(cy)} r={String(r)} fill="none" stroke="var(--border-base)" stroke-width="1" />
|
||||||
|
{isFull ? (
|
||||||
|
<circle cx={String(cx)} cy={String(cy)} r={String(r)} fill={fillColor()} opacity="0.95" />
|
||||||
|
) : sectorPath ? (
|
||||||
|
<path d={sectorPath} fill={fillColor()} opacity="0.95" />
|
||||||
|
) : null}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipText = () => `Context Used: ${percentLabel()}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="inline-flex items-center gap-2" title={tooltipText()}>
|
||||||
|
{circle()}
|
||||||
|
<div class={containerClass}>
|
||||||
|
<span class={LABEL_CLASS}>{props.usedLabel}</span>
|
||||||
|
<span class="font-semibold text-primary tabular-nums">{props.formatTokens(used())}</span>
|
||||||
|
<span class="text-muted">/</span>
|
||||||
|
<span class={LABEL_CLASS}>{props.availableLabel}</span>
|
||||||
|
<span class="font-semibold text-primary tabular-nums">
|
||||||
|
{available() !== null ? props.formatTokens(available() as number) : "--"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMeter
|
||||||
@@ -12,6 +12,7 @@ interface MonacoDiffViewerProps {
|
|||||||
after: string
|
after: string
|
||||||
viewMode?: "split" | "unified"
|
viewMode?: "split" | "unified"
|
||||||
contextMode?: "expanded" | "collapsed"
|
contextMode?: "expanded" | "collapsed"
|
||||||
|
wordWrap?: "on" | "off"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||||
@@ -54,7 +55,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
renderWhitespace: "selection",
|
renderWhitespace: "selection",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
wordWrap: "off",
|
wordWrap: props.wordWrap === "on" ? "on" : "off",
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
folding: false,
|
folding: false,
|
||||||
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
||||||
@@ -81,6 +82,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
if (!ready() || !monaco || !diffEditor) return
|
if (!ready() || !monaco || !diffEditor) return
|
||||||
const viewMode = props.viewMode === "unified" ? "unified" : "split"
|
const viewMode = props.viewMode === "unified" ? "unified" : "split"
|
||||||
const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded"
|
const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded"
|
||||||
|
const wordWrap = props.wordWrap === "on" ? "on" : "off"
|
||||||
|
|
||||||
diffEditor.updateOptions({
|
diffEditor.updateOptions({
|
||||||
renderSideBySide: viewMode === "split",
|
renderSideBySide: viewMode === "split",
|
||||||
@@ -89,7 +91,20 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
contextMode === "collapsed"
|
contextMode === "collapsed"
|
||||||
? { enabled: true }
|
? { enabled: true }
|
||||||
: { enabled: false },
|
: { enabled: false },
|
||||||
|
wordWrap,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
diffEditor.getOriginalEditor?.()?.updateOptions?.({ wordWrap })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
diffEditor.getModifiedEditor?.()?.updateOptions?.({ wordWrap })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import PermissionNotificationBanner from "../permission-notification-banner"
|
|||||||
import PermissionApprovalModal from "../permission-approval-modal"
|
import PermissionApprovalModal from "../permission-approval-modal"
|
||||||
import SessionView from "../session/session-view"
|
import SessionView from "../session/session-view"
|
||||||
import { formatTokenTotal } from "../../lib/formatters"
|
import { formatTokenTotal } from "../../lib/formatters"
|
||||||
|
import ContextMeter from "../context-meter"
|
||||||
import { sseManager } from "../../lib/sse-manager"
|
import { sseManager } from "../../lib/sse-manager"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { serverApi } from "../../lib/api-client"
|
import { serverApi } from "../../lib/api-client"
|
||||||
@@ -41,7 +42,7 @@ import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
|||||||
import RightPanel from "./shell/right-panel/RightPanel"
|
import RightPanel from "./shell/right-panel/RightPanel"
|
||||||
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
||||||
import { getSessionStatus } from "../../stores/session-status"
|
import { getSessionStatus } from "../../stores/session-status"
|
||||||
import { ShieldAlert } from "lucide-solid"
|
import { Maximize2, ShieldAlert } from "lucide-solid"
|
||||||
|
|
||||||
import type { LayoutMode } from "./shell/types"
|
import type { LayoutMode } from "./shell/types"
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,11 @@ interface InstanceShellProps {
|
|||||||
handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise<void>
|
handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise<void>
|
||||||
onExecuteCommand: (command: Command) => void
|
onExecuteCommand: (command: Command) => void
|
||||||
tabBarOffset: number
|
tabBarOffset: number
|
||||||
|
|
||||||
|
// In-memory only: mobile immersive/fullscreen mode.
|
||||||
|
mobileFullscreenMode: boolean
|
||||||
|
onEnterMobileFullscreen: () => void
|
||||||
|
onExitMobileFullscreen: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||||
@@ -117,6 +123,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||||
|
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
||||||
|
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
||||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
|
|
||||||
@@ -349,16 +357,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
measureDrawerHost,
|
measureDrawerHost,
|
||||||
})
|
})
|
||||||
|
|
||||||
const formattedUsedTokens = () => formatTokenTotal(tokenStats().used)
|
|
||||||
|
|
||||||
|
|
||||||
const formattedAvailableTokens = () => {
|
|
||||||
const avail = tokenStats().avail
|
|
||||||
if (typeof avail === "number") {
|
|
||||||
return formatTokenTotal(avail)
|
|
||||||
}
|
|
||||||
return "--"
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderLeftPanel = () => {
|
const renderLeftPanel = () => {
|
||||||
if (leftPinned()) {
|
if (leftPinned()) {
|
||||||
@@ -594,13 +592,14 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
{renderLeftPanel()}
|
{renderLeftPanel()}
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0, minHeight: 0, overflowX: "hidden" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0, minHeight: 0, overflowX: "hidden" }}>
|
||||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
<Show when={!mobileFullscreen()}>
|
||||||
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
||||||
<Show
|
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
||||||
when={!isPhoneLayout()}
|
<Show
|
||||||
fallback={
|
when={!isPhoneLayout()}
|
||||||
<div class="flex flex-col w-full gap-1.5">
|
fallback={
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
<div class="flex flex-col w-full gap-1.5">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||||
<Show when={leftDrawerState() === "floating-closed"}>
|
<Show when={leftDrawerState() === "floating-closed"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={setLeftToggleButtonEl}
|
ref={setLeftToggleButtonEl}
|
||||||
@@ -647,6 +646,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={!props.mobileFullscreenMode}>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
onClick={props.onEnterMobileFullscreen}
|
||||||
|
aria-label={t("instanceShell.fullscreen.enter")}
|
||||||
|
title={t("instanceShell.fullscreen.enter")}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Maximize2 class="w-5 h-5" aria-hidden="true" />
|
||||||
|
</IconButton>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={rightDrawerState() === "floating-closed"}>
|
<Show when={rightDrawerState() === "floating-closed"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={setRightToggleButtonEl}
|
ref={setRightToggleButtonEl}
|
||||||
@@ -661,20 +672,15 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<ContextMeter
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
usedTokens={tokenStats().used}
|
||||||
{t("instanceShell.metrics.usedLabel")}
|
availableTokens={tokenStats().avail}
|
||||||
</span>
|
formatTokens={formatTokenTotal}
|
||||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||||
|
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
|
||||||
{t("instanceShell.metrics.availableLabel")}
|
|
||||||
</span>
|
|
||||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -693,18 +699,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!showingInfoView()}>
|
<Show when={!showingInfoView()}>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<ContextMeter
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
usedTokens={tokenStats().used}
|
||||||
{t("instanceShell.metrics.usedLabel")}
|
availableTokens={tokenStats().avail}
|
||||||
</span>
|
formatTokens={formatTokenTotal}
|
||||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||||
</div>
|
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
/>
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
|
||||||
{t("instanceShell.metrics.availableLabel")}
|
|
||||||
</span>
|
|
||||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center session-header-hints">
|
<div class="ml-auto flex items-center session-header-hints">
|
||||||
@@ -769,9 +770,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
@@ -808,6 +810,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
instanceId={props.instance.id}
|
instanceId={props.instance.id}
|
||||||
instanceFolder={props.instance.folder}
|
instanceFolder={props.instance.folder}
|
||||||
escapeInDebounce={props.escapeInDebounce}
|
escapeInDebounce={props.escapeInDebounce}
|
||||||
|
isPhoneLayout={isPhoneLayout()}
|
||||||
|
compactPromptLayout={compactPromptLayout()}
|
||||||
showSidebarToggle={showEmbeddedSidebarToggle()}
|
showSidebarToggle={showEmbeddedSidebarToggle()}
|
||||||
onSidebarToggle={() => setLeftOpen(true)}
|
onSidebarToggle={() => setLeftOpen(true)}
|
||||||
forceCompactStatusLayout={showEmbeddedSidebarToggle()}
|
forceCompactStatusLayout={showEmbeddedSidebarToggle()}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type { Instance } from "../../../../types/instance"
|
|||||||
import type { BackgroundProcess } from "../../../../../../server/src/api-types"
|
import type { BackgroundProcess } from "../../../../../../server/src/api-types"
|
||||||
import type { Session } from "../../../../types/session"
|
import type { Session } from "../../../../types/session"
|
||||||
import type { DrawerViewState } from "../types"
|
import type { DrawerViewState } from "../types"
|
||||||
import type { DiffContextMode, DiffViewMode, RightPanelTab } from "./types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } from "./types"
|
||||||
|
|
||||||
import ChangesTab from "./tabs/ChangesTab"
|
import ChangesTab from "./tabs/ChangesTab"
|
||||||
import FilesTab from "./tabs/FilesTab"
|
import FilesTab from "./tabs/FilesTab"
|
||||||
@@ -32,6 +32,7 @@ import { useGlobalPointerDrag } from "../useGlobalPointerDrag"
|
|||||||
import {
|
import {
|
||||||
RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY,
|
RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY,
|
||||||
RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY,
|
RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY,
|
||||||
|
RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY,
|
||||||
RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY,
|
RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY,
|
||||||
RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY,
|
RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY,
|
||||||
RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY,
|
RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY,
|
||||||
@@ -102,6 +103,9 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
const [diffContextMode, setDiffContextMode] = createSignal<DiffContextMode>(
|
const [diffContextMode, setDiffContextMode] = createSignal<DiffContextMode>(
|
||||||
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, ["expanded", "collapsed"] as const) ?? "collapsed",
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, ["expanded", "collapsed"] as const) ?? "collapsed",
|
||||||
)
|
)
|
||||||
|
const [diffWordWrapMode, setDiffWordWrapMode] = createSignal<DiffWordWrapMode>(
|
||||||
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, ["on", "off"] as const) ?? "on",
|
||||||
|
)
|
||||||
|
|
||||||
const [changesSplitWidth, setChangesSplitWidth] = createSignal(320)
|
const [changesSplitWidth, setChangesSplitWidth] = createSignal(320)
|
||||||
const [filesSplitWidth, setFilesSplitWidth] = createSignal(320)
|
const [filesSplitWidth, setFilesSplitWidth] = createSignal(320)
|
||||||
@@ -195,6 +199,11 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, diffContextMode())
|
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, diffContextMode())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, diffWordWrapMode())
|
||||||
|
})
|
||||||
|
|
||||||
const clampSplitWidth = (value: number) => {
|
const clampSplitWidth = (value: number) => {
|
||||||
const min = 200
|
const min = 200
|
||||||
const maxByDrawer = Math.max(min, Math.floor(props.rightDrawerWidth() * 0.65))
|
const maxByDrawer = Math.max(min, Math.floor(props.rightDrawerWidth() * 0.65))
|
||||||
@@ -738,8 +747,10 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
onSelectFile={handleSelectChangesFile}
|
onSelectFile={handleSelectChangesFile}
|
||||||
diffViewMode={diffViewMode}
|
diffViewMode={diffViewMode}
|
||||||
diffContextMode={diffContextMode}
|
diffContextMode={diffContextMode}
|
||||||
|
diffWordWrapMode={diffWordWrapMode}
|
||||||
onViewModeChange={setDiffViewMode}
|
onViewModeChange={setDiffViewMode}
|
||||||
onContextModeChange={setDiffContextMode}
|
onContextModeChange={setDiffContextMode}
|
||||||
|
onWordWrapModeChange={setDiffWordWrapMode}
|
||||||
listOpen={changesListOpen}
|
listOpen={changesListOpen}
|
||||||
onToggleList={toggleChangesList}
|
onToggleList={toggleChangesList}
|
||||||
splitWidth={changesSplitWidth}
|
splitWidth={changesSplitWidth}
|
||||||
@@ -765,8 +776,10 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
scopeKey={gitScopeKey}
|
scopeKey={gitScopeKey}
|
||||||
diffViewMode={diffViewMode}
|
diffViewMode={diffViewMode}
|
||||||
diffContextMode={diffContextMode}
|
diffContextMode={diffContextMode}
|
||||||
|
diffWordWrapMode={diffWordWrapMode}
|
||||||
onViewModeChange={setDiffViewMode}
|
onViewModeChange={setDiffViewMode}
|
||||||
onContextModeChange={setDiffContextMode}
|
onContextModeChange={setDiffContextMode}
|
||||||
|
onWordWrapModeChange={setDiffWordWrapMode}
|
||||||
onOpenFile={(path) => void openGitFile(path)}
|
onOpenFile={(path) => void openGitFile(path)}
|
||||||
onRefresh={() => void refreshGitStatus()}
|
onRefresh={() => void refreshGitStatus()}
|
||||||
listOpen={gitChangesListOpen}
|
listOpen={gitChangesListOpen}
|
||||||
|
|||||||
@@ -1,50 +1,61 @@
|
|||||||
import type { Component } from "solid-js"
|
import type { Component } from "solid-js"
|
||||||
|
|
||||||
import type { DiffContextMode, DiffViewMode } from "../types"
|
import { AlignJustify, FoldVertical, Split, UnfoldVertical, WrapText } from "lucide-solid"
|
||||||
|
|
||||||
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface DiffToolbarProps {
|
interface DiffToolbarProps {
|
||||||
viewMode: DiffViewMode
|
viewMode: DiffViewMode
|
||||||
contextMode: DiffContextMode
|
contextMode: DiffContextMode
|
||||||
|
wordWrapMode: DiffWordWrapMode
|
||||||
onViewModeChange: (mode: DiffViewMode) => void
|
onViewModeChange: (mode: DiffViewMode) => void
|
||||||
onContextModeChange: (mode: DiffContextMode) => void
|
onContextModeChange: (mode: DiffContextMode) => void
|
||||||
|
onWordWrapModeChange: (mode: DiffWordWrapMode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiffToolbar: Component<DiffToolbarProps> = (props) => {
|
const DiffToolbar: Component<DiffToolbarProps> = (props) => {
|
||||||
|
const nextViewMode = (): DiffViewMode => (props.viewMode === "split" ? "unified" : "split")
|
||||||
|
const nextContextMode = (): DiffContextMode => (props.contextMode === "collapsed" ? "expanded" : "collapsed")
|
||||||
|
const nextWordWrapMode = (): DiffWordWrapMode => (props.wordWrapMode === "on" ? "off" : "on")
|
||||||
|
|
||||||
|
const viewModeTitle = () => (nextViewMode() === "split" ? "Switch to split view" : "Switch to unified view")
|
||||||
|
const contextModeTitle = () =>
|
||||||
|
nextContextMode() === "collapsed" ? "Hide unchanged regions" : "Show full file"
|
||||||
|
const wordWrapTitle = () => (nextWordWrapMode() === "on" ? "Enable word wrap" : "Disable word wrap")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="file-viewer-toolbar">
|
<div class="file-viewer-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`file-viewer-toolbar-button${props.viewMode === "split" ? " active" : ""}`}
|
class="file-viewer-toolbar-icon-button"
|
||||||
aria-pressed={props.viewMode === "split"}
|
onClick={() => props.onViewModeChange(nextViewMode())}
|
||||||
onClick={() => props.onViewModeChange("split")}
|
aria-label={viewModeTitle()}
|
||||||
|
title={viewModeTitle()}
|
||||||
>
|
>
|
||||||
Split
|
{nextViewMode() === "split" ? <Split class="h-4 w-4" aria-hidden="true" /> : <AlignJustify class="h-4 w-4" aria-hidden="true" />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`file-viewer-toolbar-button${props.viewMode === "unified" ? " active" : ""}`}
|
class="file-viewer-toolbar-icon-button"
|
||||||
aria-pressed={props.viewMode === "unified"}
|
onClick={() => props.onContextModeChange(nextContextMode())}
|
||||||
onClick={() => props.onViewModeChange("unified")}
|
aria-label={contextModeTitle()}
|
||||||
|
title={contextModeTitle()}
|
||||||
>
|
>
|
||||||
Unified
|
{nextContextMode() === "collapsed" ? (
|
||||||
|
<FoldVertical class="h-4 w-4" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<UnfoldVertical class="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`file-viewer-toolbar-button${props.contextMode === "collapsed" ? " active" : ""}`}
|
class={`file-viewer-toolbar-icon-button${props.wordWrapMode === "on" ? " active" : ""}`}
|
||||||
aria-pressed={props.contextMode === "collapsed"}
|
onClick={() => props.onWordWrapModeChange(nextWordWrapMode())}
|
||||||
onClick={() => props.onContextModeChange("collapsed")}
|
aria-label={wordWrapTitle()}
|
||||||
title="Hide unchanged regions"
|
title={wordWrapTitle()}
|
||||||
>
|
>
|
||||||
Collapsed
|
<WrapText class="h-4 w-4" aria-hidden="true" />
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`file-viewer-toolbar-button${props.contextMode === "expanded" ? " active" : ""}`}
|
|
||||||
aria-pressed={props.contextMode === "expanded"}
|
|
||||||
onClick={() => props.onContextModeChange("expanded")}
|
|
||||||
title="Show full file"
|
|
||||||
>
|
|
||||||
Expanded
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
|||||||
|
|
||||||
import DiffToolbar from "../components/DiffToolbar"
|
import DiffToolbar from "../components/DiffToolbar"
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
import type { DiffContextMode, DiffViewMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
@@ -18,8 +18,10 @@ interface ChangesTabProps {
|
|||||||
|
|
||||||
diffViewMode: Accessor<DiffViewMode>
|
diffViewMode: Accessor<DiffViewMode>
|
||||||
diffContextMode: Accessor<DiffContextMode>
|
diffContextMode: Accessor<DiffContextMode>
|
||||||
|
diffWordWrapMode: Accessor<DiffWordWrapMode>
|
||||||
onViewModeChange: (mode: DiffViewMode) => void
|
onViewModeChange: (mode: DiffViewMode) => void
|
||||||
onContextModeChange: (mode: DiffContextMode) => void
|
onContextModeChange: (mode: DiffContextMode) => void
|
||||||
|
onWordWrapModeChange: (mode: DiffWordWrapMode) => void
|
||||||
|
|
||||||
listOpen: Accessor<boolean>
|
listOpen: Accessor<boolean>
|
||||||
onToggleList: () => void
|
onToggleList: () => void
|
||||||
@@ -77,14 +79,6 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
|
||||||
<DiffToolbar
|
|
||||||
viewMode={props.diffViewMode()}
|
|
||||||
contextMode={props.diffContextMode()}
|
|
||||||
onViewModeChange={props.onViewModeChange}
|
|
||||||
onContextModeChange={props.onContextModeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="file-viewer-content file-viewer-content--monaco">
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
<Show
|
<Show
|
||||||
when={selectedFileData && hasSession && Array.isArray(diffs) && diffs.length > 0 ? selectedFileData : null}
|
when={selectedFileData && hasSession && Array.isArray(diffs) && diffs.length > 0 ? selectedFileData : null}
|
||||||
@@ -102,6 +96,7 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
after={String((file() as any).after || "")}
|
after={String((file() as any).after || "")}
|
||||||
viewMode={props.diffViewMode()}
|
viewMode={props.diffViewMode()}
|
||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrap={props.diffWordWrapMode()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -182,6 +177,17 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ "margin-left": "auto" }}>
|
||||||
|
<DiffToolbar
|
||||||
|
viewMode={props.diffViewMode()}
|
||||||
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrapMode={props.diffWordWrapMode()}
|
||||||
|
onViewModeChange={props.onViewModeChange}
|
||||||
|
onContextModeChange={props.onContextModeChange}
|
||||||
|
onWordWrapModeChange={props.onWordWrapModeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
|||||||
|
|
||||||
import DiffToolbar from "../components/DiffToolbar"
|
import DiffToolbar from "../components/DiffToolbar"
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
import type { DiffContextMode, DiffViewMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface GitChangesTabProps {
|
interface GitChangesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
@@ -29,8 +29,10 @@ interface GitChangesTabProps {
|
|||||||
|
|
||||||
diffViewMode: Accessor<DiffViewMode>
|
diffViewMode: Accessor<DiffViewMode>
|
||||||
diffContextMode: Accessor<DiffContextMode>
|
diffContextMode: Accessor<DiffContextMode>
|
||||||
|
diffWordWrapMode: Accessor<DiffWordWrapMode>
|
||||||
onViewModeChange: (mode: DiffViewMode) => void
|
onViewModeChange: (mode: DiffViewMode) => void
|
||||||
onContextModeChange: (mode: DiffContextMode) => void
|
onContextModeChange: (mode: DiffContextMode) => void
|
||||||
|
onWordWrapModeChange: (mode: DiffWordWrapMode) => void
|
||||||
|
|
||||||
onOpenFile: (path: string) => void
|
onOpenFile: (path: string) => void
|
||||||
onRefresh: () => void
|
onRefresh: () => void
|
||||||
@@ -80,14 +82,6 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
|
||||||
<DiffToolbar
|
|
||||||
viewMode={props.diffViewMode()}
|
|
||||||
contextMode={props.diffContextMode()}
|
|
||||||
onViewModeChange={props.onViewModeChange}
|
|
||||||
onContextModeChange={props.onContextModeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="file-viewer-content file-viewer-content--monaco">
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
<Show
|
<Show
|
||||||
when={props.selectedLoading()}
|
when={props.selectedLoading()}
|
||||||
@@ -122,6 +116,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
after={String((file() as any).after || "")}
|
after={String((file() as any).after || "")}
|
||||||
viewMode={props.diffViewMode()}
|
viewMode={props.diffViewMode()}
|
||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrap={props.diffWordWrapMode()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -237,6 +232,15 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<RefreshCw class={`h-4 w-4${props.statusLoading() ? " animate-spin" : ""}`} />
|
<RefreshCw class={`h-4 w-4${props.statusLoading() ? " animate-spin" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<DiffToolbar
|
||||||
|
viewMode={props.diffViewMode()}
|
||||||
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrapMode={props.diffWordWrapMode()}
|
||||||
|
onViewModeChange={props.onViewModeChange}
|
||||||
|
onContextModeChange={props.onContextModeChange}
|
||||||
|
onWordWrapModeChange={props.onWordWrapModeChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export type RightPanelTab = "changes" | "git-changes" | "files" | "status"
|
|||||||
export type DiffViewMode = "split" | "unified"
|
export type DiffViewMode = "split" | "unified"
|
||||||
|
|
||||||
export type DiffContextMode = "expanded" | "collapsed"
|
export type DiffContextMode = "expanded" | "collapsed"
|
||||||
|
|
||||||
|
export type DiffWordWrapMode = "on" | "off"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_NONPHONE_KEY = "opencode-session-
|
|||||||
export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-git-changes-list-open-phone-v1"
|
export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-git-changes-list-open-phone-v1"
|
||||||
export const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1"
|
export const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1"
|
||||||
export const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1"
|
export const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1"
|
||||||
|
export const RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY = "opencode-session-right-panel-changes-diff-word-wrap-v1"
|
||||||
|
|
||||||
export const clampWidth = (value: number) =>
|
export const clampWidth = (value: number) =>
|
||||||
Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
|
import ContextMeter from "./context-meter"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
|
|
||||||
const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
|
|
||||||
|
|
||||||
interface MessageListHeaderProps {
|
interface MessageListHeaderProps {
|
||||||
usedTokens: number
|
usedTokens: number
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const hasAvailableTokens = () => typeof props.availableTokens === "number"
|
const hasAvailableTokens = () => typeof props.availableTokens === "number"
|
||||||
const availableDisplay = () => (hasAvailableTokens() ? props.formatTokens(props.availableTokens as number) : "--")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={props.forceCompactStatusLayout ? "connection-status connection-status--compact" : "connection-status"}>
|
<div class={props.forceCompactStatusLayout ? "connection-status connection-status--compact" : "connection-status"}>
|
||||||
@@ -40,14 +37,13 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
|
|||||||
|
|
||||||
<div class="connection-status-text connection-status-info">
|
<div class="connection-status-text connection-status-info">
|
||||||
<div class="connection-status-usage">
|
<div class="connection-status-usage">
|
||||||
<div class={METRIC_CHIP_CLASS}>
|
<ContextMeter
|
||||||
<span class={METRIC_LABEL_CLASS}>{t("messageListHeader.metrics.usedLabel")}</span>
|
usedTokens={props.usedTokens}
|
||||||
<span class="font-semibold text-primary">{props.formatTokens(props.usedTokens)}</span>
|
availableTokens={hasAvailableTokens() ? (props.availableTokens as number) : null}
|
||||||
</div>
|
formatTokens={props.formatTokens}
|
||||||
<div class={METRIC_CHIP_CLASS}>
|
usedLabel={t("messageListHeader.metrics.usedLabel")}
|
||||||
<span class={METRIC_LABEL_CLASS}>{t("messageListHeader.metrics.availableLabel")}</span>
|
availableLabel={t("messageListHeader.metrics.availableLabel")}
|
||||||
<span class="font-semibold text-primary">{hasAvailableTokens() ? availableDisplay() : "--"}</span>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -176,15 +176,26 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
onMount(() => {
|
const isCoarsePointer = () => {
|
||||||
|
if (typeof window === "undefined") return false
|
||||||
|
return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// Scope global "type-to-focus" behavior to the active, visible prompt only.
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
if (isCoarsePointer()) return
|
||||||
|
if (props.isActive === false) return
|
||||||
|
if (props.disabled) return
|
||||||
|
|
||||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||||
const activeElement = document.activeElement as HTMLElement
|
const activeElement = document.activeElement as HTMLElement | null
|
||||||
|
|
||||||
const isInputElement =
|
const isInputElement =
|
||||||
activeElement?.tagName === "INPUT" ||
|
activeElement?.tagName === "INPUT" ||
|
||||||
activeElement?.tagName === "TEXTAREA" ||
|
activeElement?.tagName === "TEXTAREA" ||
|
||||||
activeElement?.tagName === "SELECT" ||
|
activeElement?.tagName === "SELECT" ||
|
||||||
activeElement?.isContentEditable
|
Boolean(activeElement?.isContentEditable)
|
||||||
|
|
||||||
if (isInputElement) return
|
if (isInputElement) return
|
||||||
|
|
||||||
@@ -192,16 +203,25 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (isModifierKey) return
|
if (isModifierKey) return
|
||||||
|
|
||||||
const isSpecialKey =
|
const isSpecialKey =
|
||||||
e.key === "Tab" || e.key === "Enter" || e.key.startsWith("Arrow") || e.key === "Backspace" || e.key === "Delete"
|
e.key === "Tab" ||
|
||||||
|
e.key === "Enter" ||
|
||||||
|
e.key.startsWith("Arrow") ||
|
||||||
|
e.key === "Backspace" ||
|
||||||
|
e.key === "Delete"
|
||||||
if (isSpecialKey) return
|
if (isSpecialKey) return
|
||||||
|
|
||||||
if (e.key.length === 1 && textareaRef && !props.disabled) {
|
const textarea = textareaRef
|
||||||
textareaRef.focus()
|
if (!textarea || textarea.disabled) return
|
||||||
|
|
||||||
|
// In session cache mode inactive panes are display:none; avoid stealing focus.
|
||||||
|
if (textarea.offsetParent === null) return
|
||||||
|
|
||||||
|
if (e.key.length === 1) {
|
||||||
|
textarea.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", handleGlobalKeyDown)
|
document.addEventListener("keydown", handleGlobalKeyDown)
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
document.removeEventListener("keydown", handleGlobalKeyDown)
|
document.removeEventListener("keydown", handleGlobalKeyDown)
|
||||||
})
|
})
|
||||||
@@ -435,7 +455,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
rows={expandState() === "expanded" ? 15 : 4}
|
rows={expandState() === "expanded" ? (props.compactLayout ? 10 : 15) : 3}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export interface PromptInputProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
instanceFolder: string
|
instanceFolder: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
|
||||||
|
// Used to scope global "type-to-focus" behavior.
|
||||||
|
isActive?: boolean
|
||||||
|
|
||||||
|
// Phone/tablet layouts should keep the expanded prompt more compact.
|
||||||
|
compactLayout?: boolean
|
||||||
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
|
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
|
||||||
onRunShell?: (command: string) => Promise<void>
|
onRunShell?: (command: string) => Promise<void>
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
const [meta, setMeta] = createSignal<ServerMeta | null>(null)
|
const [meta, setMeta] = createSignal<ServerMeta | null>(null)
|
||||||
const [authStatus, setAuthStatus] = createSignal<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean } | null>(null)
|
const [authStatus, setAuthStatus] = createSignal<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean } | null>(null)
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
|
const [applyingListeningMode, setApplyingListeningMode] = createSignal(false)
|
||||||
const [qrCodes, setQrCodes] = createSignal<Record<string, string>>({})
|
const [qrCodes, setQrCodes] = createSignal<Record<string, string>>({})
|
||||||
const [expandedUrl, setExpandedUrl] = createSignal<string | null>(null)
|
const [expandedUrl, setExpandedUrl] = createSignal<string | null>(null)
|
||||||
const [error, setError] = createSignal<string | null>(null)
|
const [error, setError] = createSignal<string | null>(null)
|
||||||
@@ -88,6 +89,10 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (applyingListeningMode()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await showConfirmDialog(t("remoteAccess.listeningMode.restartConfirm.message"), {
|
const confirmed = await showConfirmDialog(t("remoteAccess.listeningMode.restartConfirm.message"), {
|
||||||
title: allow ? t("remoteAccess.listeningMode.restartConfirm.title.all") : t("remoteAccess.listeningMode.restartConfirm.title.local"),
|
title: allow ? t("remoteAccess.listeningMode.restartConfirm.title.all") : t("remoteAccess.listeningMode.restartConfirm.title.local"),
|
||||||
variant: "warning",
|
variant: "warning",
|
||||||
@@ -100,12 +105,21 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setListeningMode(targetMode)
|
setApplyingListeningMode(true)
|
||||||
const restarted = await restartCli()
|
setError(null)
|
||||||
if (!restarted) {
|
try {
|
||||||
setError(t("remoteAccess.restart.errorManual"))
|
// Important: await the config patch before restart so Electron reads the updated mode from disk.
|
||||||
} else {
|
await setListeningMode(targetMode)
|
||||||
setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev))
|
const restarted = await restartCli()
|
||||||
|
if (!restarted) {
|
||||||
|
setError(t("remoteAccess.restart.errorManual"))
|
||||||
|
} else {
|
||||||
|
setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
} finally {
|
||||||
|
setApplyingListeningMode(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshMeta()
|
void refreshMeta()
|
||||||
@@ -196,6 +210,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
onChange={(nextChecked) => {
|
onChange={(nextChecked) => {
|
||||||
void handleAllowConnectionsChange(nextChecked)
|
void handleAllowConnectionsChange(nextChecked)
|
||||||
}}
|
}}
|
||||||
|
disabled={loading() || applyingListeningMode()}
|
||||||
>
|
>
|
||||||
<Switch.Input />
|
<Switch.Input />
|
||||||
<Switch.Control class="remote-toggle-switch" data-checked={allowExternalConnections()}>
|
<Switch.Control class="remote-toggle-switch" data-checked={allowExternalConnections()}>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ interface SessionViewProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
instanceFolder: string
|
instanceFolder: string
|
||||||
escapeInDebounce: boolean
|
escapeInDebounce: boolean
|
||||||
|
isPhoneLayout?: boolean
|
||||||
|
compactPromptLayout?: boolean
|
||||||
showSidebarToggle?: boolean
|
showSidebarToggle?: boolean
|
||||||
onSidebarToggle?: () => void
|
onSidebarToggle?: () => void
|
||||||
forceCompactStatusLayout?: boolean
|
forceCompactStatusLayout?: boolean
|
||||||
@@ -76,6 +78,9 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
(isActive) => {
|
(isActive) => {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
|
|
||||||
|
// On phones, focusing the prompt on session switch is disruptive (it raises the OSK).
|
||||||
|
if (props.isPhoneLayout) return
|
||||||
|
|
||||||
// Don't steal focus from other inputs (command palette, dialogs, selectors, etc.)
|
// Don't steal focus from other inputs (command palette, dialogs, selectors, etc.)
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const activeEl = document.activeElement as HTMLElement | null
|
const activeEl = document.activeElement as HTMLElement | null
|
||||||
@@ -314,17 +319,19 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<PromptInput
|
<PromptInput
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
instanceFolder={props.instanceFolder}
|
instanceFolder={props.instanceFolder}
|
||||||
sessionId={activeSession.id}
|
sessionId={activeSession.id}
|
||||||
onSend={handleSendMessage}
|
isActive={props.isActive}
|
||||||
onRunShell={handleRunShell}
|
compactLayout={props.compactPromptLayout}
|
||||||
escapeInDebounce={props.escapeInDebounce}
|
onSend={handleSendMessage}
|
||||||
isSessionBusy={sessionBusy()}
|
onRunShell={handleRunShell}
|
||||||
disabled={sessionNeedsInput()}
|
escapeInDebounce={props.escapeInDebounce}
|
||||||
onAbortSession={handleAbortSession}
|
isSessionBusy={sessionBusy()}
|
||||||
registerPromptInputApi={registerPromptInputApi}
|
disabled={sessionNeedsInput()}
|
||||||
/>
|
onAbortSession={handleAbortSession}
|
||||||
|
registerPromptInputApi={registerPromptInputApi}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Open right drawer",
|
"instanceShell.rightDrawer.toggle.open": "Open right drawer",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Close right drawer",
|
"instanceShell.rightDrawer.toggle.close": "Close right drawer",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Full screen",
|
||||||
|
"instanceShell.fullscreen.exit": "Exit full screen",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Used",
|
"instanceShell.metrics.usedLabel": "Used",
|
||||||
"instanceShell.metrics.availableLabel": "Avail",
|
"instanceShell.metrics.availableLabel": "Avail",
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Abrir panel derecho",
|
"instanceShell.rightDrawer.toggle.open": "Abrir panel derecho",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Cerrar panel derecho",
|
"instanceShell.rightDrawer.toggle.close": "Cerrar panel derecho",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Pantalla completa",
|
||||||
|
"instanceShell.fullscreen.exit": "Salir de pantalla completa",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Usado",
|
"instanceShell.metrics.usedLabel": "Usado",
|
||||||
"instanceShell.metrics.availableLabel": "Disp.",
|
"instanceShell.metrics.availableLabel": "Disp.",
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Ouvrir le tiroir droit",
|
"instanceShell.rightDrawer.toggle.open": "Ouvrir le tiroir droit",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Fermer le tiroir droit",
|
"instanceShell.rightDrawer.toggle.close": "Fermer le tiroir droit",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Plein écran",
|
||||||
|
"instanceShell.fullscreen.exit": "Quitter le plein écran",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Utilisé",
|
"instanceShell.metrics.usedLabel": "Utilisé",
|
||||||
"instanceShell.metrics.availableLabel": "Dispo",
|
"instanceShell.metrics.availableLabel": "Dispo",
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "右ドロワーを開く",
|
"instanceShell.rightDrawer.toggle.open": "右ドロワーを開く",
|
||||||
"instanceShell.rightDrawer.toggle.close": "右ドロワーを閉じる",
|
"instanceShell.rightDrawer.toggle.close": "右ドロワーを閉じる",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "全画面",
|
||||||
|
"instanceShell.fullscreen.exit": "全画面を終了",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "使用",
|
"instanceShell.metrics.usedLabel": "使用",
|
||||||
"instanceShell.metrics.availableLabel": "残り",
|
"instanceShell.metrics.availableLabel": "残り",
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Открыть правую панель",
|
"instanceShell.rightDrawer.toggle.open": "Открыть правую панель",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Закрыть правую панель",
|
"instanceShell.rightDrawer.toggle.close": "Закрыть правую панель",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Полный экран",
|
||||||
|
"instanceShell.fullscreen.exit": "Выйти из полного экрана",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Использовано",
|
"instanceShell.metrics.usedLabel": "Использовано",
|
||||||
"instanceShell.metrics.availableLabel": "Доступно",
|
"instanceShell.metrics.availableLabel": "Доступно",
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "打开右侧抽屉",
|
"instanceShell.rightDrawer.toggle.open": "打开右侧抽屉",
|
||||||
"instanceShell.rightDrawer.toggle.close": "关闭右侧抽屉",
|
"instanceShell.rightDrawer.toggle.close": "关闭右侧抽屉",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "全屏",
|
||||||
|
"instanceShell.fullscreen.exit": "退出全屏",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "已用",
|
"instanceShell.metrics.usedLabel": "已用",
|
||||||
"instanceShell.metrics.availableLabel": "可用",
|
"instanceShell.metrics.availableLabel": "可用",
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,14 @@ export function updateSessionInfo(instanceId: string, sessionId: string): void {
|
|||||||
resolveSelectedModel(instanceProviders, latestProviderId, latestModelId)
|
resolveSelectedModel(instanceProviders, latestProviderId, latestModelId)
|
||||||
|
|
||||||
let modelOutputLimit = DEFAULT_MODEL_OUTPUT_LIMIT
|
let modelOutputLimit = DEFAULT_MODEL_OUTPUT_LIMIT
|
||||||
|
let modelInputLimit: number | null = null
|
||||||
|
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
contextWindow = selectedModel.limit?.context ?? 0
|
contextWindow = selectedModel.limit?.context ?? 0
|
||||||
|
const inputLimit = selectedModel.limit?.input
|
||||||
|
if (typeof inputLimit === "number" && inputLimit > 0) {
|
||||||
|
modelInputLimit = inputLimit
|
||||||
|
}
|
||||||
const outputLimit = selectedModel.limit?.output
|
const outputLimit = selectedModel.limit?.output
|
||||||
if (typeof outputLimit === "number" && outputLimit > 0) {
|
if (typeof outputLimit === "number" && outputLimit > 0) {
|
||||||
modelOutputLimit = Math.min(outputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
modelOutputLimit = Math.min(outputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
||||||
@@ -107,7 +112,13 @@ export function updateSessionInfo(instanceId: string, sessionId: string): void {
|
|||||||
|
|
||||||
const outputBudget = Math.min(modelOutputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
const outputBudget = Math.min(modelOutputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
||||||
|
|
||||||
if (!contextAvailableFromPrevious) {
|
if (modelInputLimit !== null) {
|
||||||
|
// Prefer explicit input limits when provided by the API.
|
||||||
|
// This is used by the UI "Avail" chip.
|
||||||
|
contextAvailableTokens = modelInputLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextAvailableFromPrevious && contextAvailableTokens === null) {
|
||||||
if (contextWindow > 0) {
|
if (contextWindow > 0) {
|
||||||
if (latestHasContextUsage && actualUsageTokens > 0) {
|
if (latestHasContextUsage && actualUsageTokens > 0) {
|
||||||
contextAvailableTokens = Math.max(contextWindow - (actualUsageTokens + outputBudget), 0)
|
contextAvailableTokens = Math.max(contextWindow - (actualUsageTokens + outputBudget), 0)
|
||||||
|
|||||||
@@ -304,10 +304,10 @@ function setThemePreference(preference: ThemePreference): void {
|
|||||||
void patchConfigOwner("ui", { theme: preference }).catch((error) => log.error("Failed to set theme", error))
|
void patchConfigOwner("ui", { theme: preference }).catch((error) => log.error("Failed to set theme", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
function setListeningMode(mode: ListeningMode): void {
|
async function setListeningMode(mode: ListeningMode): Promise<void> {
|
||||||
if (serverSettings().listeningMode === mode) return
|
if (serverSettings().listeningMode === mode) return
|
||||||
void patchConfigOwner("server", { listeningMode: mode }).catch((error) => log.error("Failed to set listening mode", error))
|
await patchConfigOwner("server", { listeningMode: mode })
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
||||||
void patchConfigOwner("server", { environmentVariables: envVars }).catch((error) =>
|
void patchConfigOwner("server", { environmentVariables: envVars }).catch((error) =>
|
||||||
|
|||||||
@@ -291,12 +291,13 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||||||
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
|
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
|
||||||
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
|
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
|
||||||
const initialContextWindow = initialModel?.limit?.context ?? 0
|
const initialContextWindow = initialModel?.limit?.context ?? 0
|
||||||
|
const initialInputLimit = initialModel?.limit?.input ?? 0
|
||||||
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
|
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
|
||||||
const initialOutputLimit =
|
const initialOutputLimit =
|
||||||
initialModel?.limit?.output && initialModel.limit.output > 0
|
initialModel?.limit?.output && initialModel.limit.output > 0
|
||||||
? initialModel.limit.output
|
? initialModel.limit.output
|
||||||
: DEFAULT_MODEL_OUTPUT_LIMIT
|
: DEFAULT_MODEL_OUTPUT_LIMIT
|
||||||
const initialContextAvailable = initialContextWindow > 0 ? initialContextWindow : null
|
const initialContextAvailable = initialInputLimit > 0 ? initialInputLimit : initialContextWindow > 0 ? initialContextWindow : null
|
||||||
|
|
||||||
setSessionInfoByInstance((prev) => {
|
setSessionInfoByInstance((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
@@ -398,10 +399,11 @@ async function forkSession(
|
|||||||
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
|
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
|
||||||
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
|
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
|
||||||
const forkContextWindow = forkModel?.limit?.context ?? 0
|
const forkContextWindow = forkModel?.limit?.context ?? 0
|
||||||
|
const forkInputLimit = forkModel?.limit?.input ?? 0
|
||||||
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
|
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
|
||||||
const forkOutputLimit =
|
const forkOutputLimit =
|
||||||
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
|
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
|
||||||
const forkContextAvailable = forkContextWindow > 0 ? forkContextWindow : null
|
const forkContextAvailable = forkInputLimit > 0 ? forkInputLimit : forkContextWindow > 0 ? forkContextWindow : null
|
||||||
|
|
||||||
setSessionInfoByInstance((prev) => {
|
setSessionInfoByInstance((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
|
|||||||
@@ -295,7 +295,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input {
|
.prompt-input {
|
||||||
padding-bottom: 1.5rem;
|
/* Prevent iOS Safari input zoom + keep input compact. */
|
||||||
|
font-size: 16px;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
:root {
|
||||||
|
--prompt-input-compact-height: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-wrapper {
|
||||||
|
min-height: var(--prompt-input-compact-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-field-container {
|
||||||
|
min-height: var(--prompt-input-compact-height);
|
||||||
|
height: var(--prompt-input-compact-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-field {
|
||||||
|
height: var(--prompt-input-compact-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-field-container.is-expanded,
|
||||||
|
.prompt-input-field.is-expanded {
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,9 +333,9 @@
|
|||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.prompt-input {
|
.prompt-input {
|
||||||
min-height: 64px;
|
min-height: 0;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
padding-bottom: 2.25rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input-wrapper {
|
.prompt-input-wrapper {
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar {
|
.file-viewer-toolbar {
|
||||||
@apply ml-auto flex items-center gap-1;
|
@apply flex items-center gap-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar-button {
|
.file-viewer-toolbar-button {
|
||||||
@@ -291,6 +291,22 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-viewer-toolbar-icon-button {
|
||||||
|
@apply inline-flex items-center justify-center shrink-0 w-7 h-7 border border-base transition-colors;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-toolbar-icon-button:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-toolbar-icon-button.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar-button:hover {
|
.file-viewer-toolbar-button:hover {
|
||||||
background-color: var(--surface-hover);
|
background-color: var(--surface-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|||||||
@@ -125,6 +125,18 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-fullscreen-exit-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(env(safe-area-inset-top, 0px) + 12px);
|
||||||
|
right: calc(env(safe-area-inset-right, 0px) + 12px);
|
||||||
|
z-index: 1250;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-fullscreen-exit-button {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.session-resize-handle {
|
.session-resize-handle {
|
||||||
@apply absolute top-0 w-1 h-full cursor-col-resize bg-transparent transition-colors;
|
@apply absolute top-0 w-1 h-full cursor-col-resize bg-transparent transition-colors;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface Model {
|
|||||||
variantKeys?: string[]
|
variantKeys?: string[]
|
||||||
limit?: {
|
limit?: {
|
||||||
context?: number
|
context?: number
|
||||||
|
input?: number
|
||||||
output?: number
|
output?: number
|
||||||
}
|
}
|
||||||
cost?: {
|
cost?: {
|
||||||
|
|||||||
BIN
temp/opencode-ai-sdk-1.2.6.tgz
Normal file
BIN
temp/opencode-ai-sdk-1.2.6.tgz
Normal file
Binary file not shown.
51
temp/package/package.json
Normal file
51
temp/package/package.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode-ai/sdk",
|
||||||
|
"version": "1.2.6",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsgo --noEmit",
|
||||||
|
"build": "./script/build.ts"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./client": {
|
||||||
|
"import": "./dist/client.js",
|
||||||
|
"types": "./dist/client.d.ts"
|
||||||
|
},
|
||||||
|
"./server": {
|
||||||
|
"import": "./dist/server.js",
|
||||||
|
"types": "./dist/server.d.ts"
|
||||||
|
},
|
||||||
|
"./v2": {
|
||||||
|
"import": "./dist/v2/index.js",
|
||||||
|
"types": "./dist/v2/index.d.ts"
|
||||||
|
},
|
||||||
|
"./v2/client": {
|
||||||
|
"import": "./dist/v2/client.js",
|
||||||
|
"types": "./dist/v2/client.d.ts"
|
||||||
|
},
|
||||||
|
"./v2/server": {
|
||||||
|
"import": "./dist/v2/server.js",
|
||||||
|
"types": "./dist/v2/server.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
|
"@tsconfig/node22": "22.0.2",
|
||||||
|
"@types/node": "22.13.9",
|
||||||
|
"typescript": "5.8.2",
|
||||||
|
"@typescript/native-preview": "7.0.0-dev.20251207.1"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"publishConfig": {
|
||||||
|
"directory": "dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user