Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
864974d
perf(dialog): add render window to prevent long-conversation DOM freeze
Kerry1020 Jul 18, 2026
9137e49
fix(dialog): bump version to 2.0.8.12, guard template access, keyboar…
Kerry1020 Jul 18, 2026
00b3524
fix(dialog): prevent q-page scroll on keyboard to stop dock flying
Kerry1020 Jul 18, 2026
42e90de
fix(css): smooth keyboard transition on composer dock
Kerry1020 Jul 18, 2026
35f70f8
fix(css): fixed positioning for composer to prevent keyboard fly-up
Kerry1020 Jul 18, 2026
950972d
fix(css): remove --sab padding from fixed composer
Kerry1020 Jul 18, 2026
9e177d0
fix(css): prevent page scroll on keyboard focus
Kerry1020 Jul 18, 2026
d573ae7
fix(dialog): use 100dvh instead of pageFhStyle for page height
Kerry1020 Jul 18, 2026
33575b8
fix(android): anchor composer above keyboard in CSS pixels
Kerry1020 Jul 18, 2026
395e6a7
fix(android): update dock after keyboard is fully shown
Kerry1020 Jul 18, 2026
8d14d54
fix(search): guard incomplete messages in conversation search
Kerry1020 Jul 18, 2026
7156efe
fix(runtime): harden malformed persisted data paths
Kerry1020 Jul 18, 2026
d0d4cf5
fix(dialog): guard malformed branch tree during navigation
Kerry1020 Jul 18, 2026
c27eede
fix(search): initialize result state before async load
Kerry1020 Jul 18, 2026
8ddcbb2
fix(android): repair legacy database records before render
Kerry1020 Jul 18, 2026
c7d2af2
fix: resolve '调用工具一瞬间界面崩溃' and restore real-time streaming display
Kerry1020 Jul 30, 2026
6932495
fix(android): enable WebView remote debugging on release variant
Kerry1020 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
applicationId "app.aiaw.glass"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20018
versionName "2.0.8.9"
versionCode 20019
versionName "2.0.8.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
4 changes: 2 additions & 2 deletions android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}

Expand Down
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
Expand Down
13 changes: 13 additions & 0 deletions android/app/src/main/java/app/aiaw/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
package app.aiaw;

import android.os.Bundle;
import android.view.WindowManager;
import android.webkit.WebView;

import com.getcapacitor.BridgeActivity;

public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(LocalFsPlugin.class);
// Set this before Capacitor creates the WebView; its edge-to-edge
// initialization otherwise overwrites the manifest/runtime mode.
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
super.onCreate(savedInstanceState);
// Always enable WebView remote debugging so the v2.0.8.12 release
// APK is verifiable on-device (real device + ADB forward
// tcp:9222 localabstract:webview_devtools_remote_<pid>). Capacitor
// defaults this to BuildConfig.DEBUG; we explicitly want it on
// for release too so the runtime crashes and stream rendering can
// be inspected. Harmless for end users — only a dev tool exposed
// over ADB/USB.
WebView.setWebContentsDebuggingEnabled(true);
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aiaw",
"version": "2.0.8.9",
"version": "2.0.8.12",
"description": "精心设计的 AI (LLM) 客户端。全功能,轻量级;支持多工作区、插件系统、跨平台、本地优先+实时云同步",
"productName": "AI as Workspace",
"author": "NitroRCr <i@krytro.com>",
Expand Down
34 changes: 29 additions & 5 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,45 @@ router.afterEach(to => {
}
})

// 捕获全局错误和未处理的 promise 拒绝
// Capture every unhandled rejection as early as possible (script-setup runs
// before onMounted and before most AI SDK promises are created). The "调用
// 工具一瞬间界面崩溃" symptom comes from AI SDK 5's streamText emitting a
// reject(undefined) on the underlying ReadableStream when the LLM provider
// returns an unexpected shape during a tool call. The browser's reason can be
// undefined with no stack at all, so we also grab a fresh stack trace from
// Error.captureStackTrace at the rejection point and dump everything we can.
;(function installUnhandledRejectionInstrumentation() {
if (typeof window === 'undefined') return
if ((window as any).__aiawRejHookInstalled) return
;(window as any).__aiawRejHookInstalled = true

window.addEventListener('unhandledrejection', (e) => {
const r = e.reason
try {
console.error('[FatalError unhandledrejection]', r, JSON.stringify({
type: typeof r,
isError: r instanceof Error,
keys: r && typeof r === 'object' ? Object.keys(r) : null,
ctor: r && r.constructor && r.constructor.name,
}))
} catch (logErr) {
console.error('[FatalError unhandledrejection] logging failed', logErr)
}
})
})()

onMounted(() => {
window.addEventListener('error', (e) => {
if (e.error) {
console.error('[FatalError window.error]', e.error?.stack || e.error)
fatalError.value = String(e.error?.message || e.error)
}
})
window.addEventListener('unhandledrejection', (e) => {
const r = e.reason
fatalError.value = String(r?.message || r || 'Unhandled promise rejection')
})
})

async function migrateBuiltinPluginsAtStartup() {
try {
if (!db.isOpen()) await db.open()
const assistants = await db.assistants.toArray()
for (const assistant of assistants) {
const beforeKeys = Object.keys(assistant.plugins || {})
Expand Down
4 changes: 2 additions & 2 deletions src/components/MessageInfoDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ const props = defineProps<{
message: Message
}>()

const length = computed(() => props.message.contents.filter(
c => c.type === 'assistant-message' || c.type === 'user-message'
const length = computed(() => (props.message.contents || []).filter(
c => (c.type === 'assistant-message' || c.type === 'user-message') && typeof c.text === 'string'
).reduce((prev, cur) => prev + cur.text.length, 0))
const createdAt = computed(() => idDateString(props.message.id))

Expand Down
55 changes: 41 additions & 14 deletions src/components/MessageItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ const props = defineProps<{
message: Message,
childNum: number,
scrollContainer: HTMLElement,
lazyPlainText?: boolean,
branchControl?: {
current: number,
max: number,
Expand All @@ -416,7 +417,7 @@ function moreInfo() {
}
const sourceCodeMode = ref(false)

const contents = computed(() => props.message.contents.map(x => {
const contents = computed(() => (props.message.contents || []).map(x => {
if (x.type === 'assistant-message' || x.type === 'user-message') {
return {
...x,
Expand All @@ -440,12 +441,22 @@ const emit = defineEmits<{

watchEffect(async () => {
const sessionId = props.message.generatingSession
if (sessionId) {
!await sessions.ping(sessionId) && db.messages.update(props.message.id, {
if (!sessionId) return
try {
const alive = await sessions.ping(sessionId)
if (alive) return
// Peer session is gone (closed tab, navigation, abort). Mark the
// message as failed and any tool call that was still in 'calling' as
// aborted. Wrap in try/catch — if the message was deleted underneath
// us, Dexie throws DataError and we must not let it bubble up to
// window.unhandledrejection (which would replace the whole UI with
// the App.vue fatal-error overlay). That's the
// "Agent 调用工具一瞬间界面崩溃" symptom on v2.0.8.12.
await db.messages.update(props.message.id, {
generatingSession: null,
status: 'failed',
error: 'aborted',
contents: props.message.contents.map(content => {
contents: (props.message.contents || []).map(content => {
if (content.type === 'assistant-tool' && content.status === 'calling') {
return {
...content,
Expand All @@ -456,11 +467,16 @@ watchEffect(async () => {
return content
}) as MessageContent[]
})
} catch (e) {
console.warn('[MessageItem] session ping/update failed', e)
}
})

const textIndex = computed(() => props.message.contents.findIndex(c => ['user-message', 'assistant-message'].includes(c.type)))
const textContent = computed(() => (props.message.contents[textIndex.value] as UserMessageContent | AssistantMessageContent))
const textIndex = computed(() => (props.message.contents || []).findIndex(c => ['user-message', 'assistant-message'].includes(c.type)))
const textContent = computed(() => {
const content = (props.message.contents || [])[textIndex.value]
return (content || { type: 'assistant-message', text: '' }) as UserMessageContent | AssistantMessageContent
})

const { perfs } = useUserPerfsStore()
const assistantsStore = useAssistantsStore()
Expand Down Expand Up @@ -517,6 +533,10 @@ function isStreamingAssistantText(content: MessageContent) {

function getStreamingRenderState(content: MessageContent) {
if (isStreamingAssistantText(content)) return streamingRenderState.value
// Off-screen messages: skip expensive KaTeX/markdown rendering
if (props.lazyPlainText && (content.type === 'assistant-message' || content.type === 'user-message')) {
return { mode: 'plain-text' as const, text: content.text }
}
if (content.type === 'assistant-message' || content.type === 'user-message') {
return { mode: 'final' as const, text: content.text }
}
Expand Down Expand Up @@ -602,7 +622,9 @@ function onSelect(mode: 'mouse' | 'touch') {
}
const range = selection.getRangeAt(0)
const targetRects = range.getBoundingClientRect()
const baseRects = textDiv.value[0].getBoundingClientRect()
const textElement = textDiv.value?.[0] as HTMLElement | undefined
if (!textElement) return
const baseRects = textElement.getBoundingClientRect()
floatBtnStyle.top = targetRects.top < 48 || mode === 'touch'
? targetRects.bottom - baseRects.top + 12 + 'px'
: targetRects.top - baseRects.top - 48 + 'px'
Expand Down Expand Up @@ -776,7 +798,7 @@ function shouldPromoteInlineMath(inlineMath: HTMLElement) {
}

function injectDisplayMathScroll() {
const el: HTMLElement = textDiv.value[0]
const el = textDiv.value?.[0] as HTMLElement | undefined
if (!el) return

clearDisplayMathScroll(el)
Expand All @@ -802,25 +824,30 @@ function injectDisplayMathScroll() {
}
function injectConvertArtifact() {
if (!isPlatformEnabled(perfs.artifactsEnabled)) return
const el: HTMLElement = textDiv.value[0]
const el = textDiv.value?.[0] as HTMLElement | undefined
if (!el) return
el.querySelectorAll('.md-editor-code').forEach(code => {
if (code.querySelector('.md-editor-convert-artifact')) return
const anchor = code.querySelector('.md-editor-collapse-tips')
const action = code.querySelector('.md-editor-code-action')
const source = code.querySelector('pre code')
if (!anchor || !action || !source) return
const btn = document.createElement('span')
btn.innerHTML = 'convert_to_text'
btn.classList.add('md-editor-convert-artifact')
btn.addEventListener('click', (ev) => {
ev.preventDefault()
ev.stopPropagation()
const text = code.querySelector('pre code').textContent
const lang = code.querySelector('pre code').getAttribute('language')
const text = source.textContent || ''
const lang = source.getAttribute('language') || ''
const pattern = new RegExp(`\`{3,}.*\\n${escapeRegex(text)}\\s*\`{3,}`, 'g')
convertArtifact(text, pattern, lang)
})
btn.title = t('messageItem.convertToArtifactBtn')
code.querySelector('.md-editor-code-action').insertBefore(btn, anchor)
code.querySelector<HTMLElement>('.md-editor-copy-button').title = t('messageItem.copyCode')
code.querySelector<HTMLElement>('.md-editor-collapse-tips').title = t('messageItem.fold')
action.insertBefore(btn, anchor)
const copyButton = code.querySelector<HTMLElement>('.md-editor-copy-button')
if (copyButton) copyButton.title = t('messageItem.copyCode')
anchor.title = t('messageItem.fold')
})
}
const mdPreviewProps = useMdPreviewProps()
Expand Down
2 changes: 1 addition & 1 deletion src/components/ModelItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ const avatar = computed<Avatar>(() => {
if (m.startsWith('grok') || m.startsWith('xai')) return { type: 'svg', name: 'grok' }
if (m.startsWith('kimi') || m.startsWith('moonshot')) return { type: 'svg', name: 'kimi-c' }
if (m.startsWith('doubao')) return { type: 'svg', name: 'doubao-c' }
return defaultAvatar(m[0].toUpperCase())
return defaultAvatar(m?.[0]?.toUpperCase() || '?')
})
</script>
3 changes: 2 additions & 1 deletion src/components/ParseFilesDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async function parse() {
if (!value) return []
const file = props.files[index]
const fp = fileparsers.value.find(fp => fp.id === value)
if (!fp || !file) return []
try {
const result = await fp.execute({ file, range: ranges[index] }, fp.settings)
return result.map(r => ({ ...r, name: file.name }))
Expand All @@ -178,7 +179,7 @@ async function parse() {
}

const ranges = reactive(props.files.map(() => null))
const selected = reactive(props.files.map((val, index) => allOptions.value[index][0]))
const selected = reactive(props.files.map((val, index) => allOptions.value[index]?.[0]).filter(Boolean))

const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
</script>
Loading