pinokiod 7.5.13 → 7.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/kernel/gpu/amd.js +30 -43
  2. package/kernel/gpu/amd_gfx_targets.json +67 -0
  3. package/kernel/gpu/nvidia.js +90 -4
  4. package/kernel/index.js +4 -3
  5. package/kernel/sysinfo.js +16 -25
  6. package/package.json +2 -1
  7. package/script/update-amd-gfx-targets.js +119 -0
  8. package/server/lib/app_log_report.js +1 -2
  9. package/server/public/logs.js +230 -30
  10. package/server/public/style.css +28 -12
  11. package/server/views/index.ejs +7 -0
  12. package/test/amd-gpu-target.test.js +169 -0
  13. package/test/gpu-target-template-variable.test.js +69 -0
  14. package/test/logs-ask-ai.test.js +65 -10
  15. package/test/nvidia-gpu-target.test.js +98 -0
  16. package/test/sysinfo-gpu-driver.test.js +134 -1
  17. package/kernel/gpu/apple.js +0 -8
  18. package/kernel/gpu/intel.js +0 -47
  19. package/release_notes/8.0.0/assets/app-ask-ai-drawer.png +0 -0
  20. package/release_notes/8.0.0/assets/app-detail-header-actions.png +0 -0
  21. package/release_notes/8.0.0/assets/autolaunch-dependencies.png +0 -0
  22. package/release_notes/8.0.0/assets/connect-huggingface-login.png +0 -0
  23. package/release_notes/8.0.0/assets/home-app-actions.png +0 -0
  24. package/release_notes/8.0.0/assets/home-sidebar-apps.png +0 -0
  25. package/release_notes/8.0.0/assets/logs-terminal-reporting.png +0 -0
  26. package/release_notes/8.0.0/assets/mobile-home-footer-nav.png +0 -0
  27. package/release_notes/8.0.0/assets/network-phone-access.png +0 -0
  28. package/release_notes/8.0.0/assets/plugins-agent-tools.png +0 -0
  29. package/release_notes/8.0.0/assets/skills-management.png +0 -0
  30. package/release_notes/8.0.0/assets/tools-runtime-management.png +0 -0
  31. package/release_notes/8.0.0/ui.md +0 -91
@@ -22,6 +22,7 @@
22
22
  const DRAFT_TITLE_NOISE_PATTERN = /^\s*(?:<<PINOKIO_SHELL>>|={6,}|-{6,}|\[api\s+local\.set\]|The default interactive shell is now|To update your account|For more details, please visit)/i
23
23
  const ASK_AI_DEFAULT_PROMPT = 'Investigate what went wrong. Inspect the app logs and explain the likely root cause and next fix.'
24
24
  const TOOL_PREFERENCE_KEY = 'pinokio.universalLauncher.tool'
25
+ const ASK_AI_TOOL_CATEGORY_ORDER = ['Terminal', 'Desktop']
25
26
  const textEncoder = typeof TextEncoder === 'function' ? new TextEncoder() : null
26
27
 
27
28
  const safeJsonParse = (value) => {
@@ -152,8 +153,10 @@
152
153
  installed: typeof plugin.installed === 'boolean' ? plugin.installed : null
153
154
  }
154
155
  }).filter(Boolean).sort((a, b) => {
155
- const categoryDelta = String(a.category || '').localeCompare(String(b.category || ''), undefined, { sensitivity: 'base' })
156
+ const categoryDelta = askAiToolCategoryRank(a.category) - askAiToolCategoryRank(b.category)
156
157
  if (categoryDelta !== 0) return categoryDelta
158
+ const categoryNameDelta = String(a.category || '').localeCompare(String(b.category || ''), undefined, { sensitivity: 'base' })
159
+ if (categoryNameDelta !== 0) return categoryNameDelta
157
160
  return String(a.label || '').localeCompare(String(b.label || ''), undefined, { sensitivity: 'base' })
158
161
  })
159
162
  }
@@ -170,6 +173,12 @@
170
173
  return `${label} (${category})`
171
174
  }
172
175
 
176
+ const askAiToolCategoryRank = (category) => {
177
+ const normalized = String(category || '').trim().toLowerCase()
178
+ const index = ASK_AI_TOOL_CATEGORY_ORDER.findIndex((item) => item.toLowerCase() === normalized)
179
+ return index >= 0 ? index : ASK_AI_TOOL_CATEGORY_ORDER.length
180
+ }
181
+
173
182
  class PrivacyFilterClient {
174
183
  constructor() {
175
184
  this.worker = null
@@ -695,9 +704,11 @@
695
704
  toolPicker.className = 'universal-launcher-tool-picker logs-ask-ai-tool-picker'
696
705
  toolSection.appendChild(toolPicker)
697
706
 
698
- const toolTrigger = document.createElement('div')
707
+ const toolTrigger = document.createElement('button')
708
+ toolTrigger.type = 'button'
699
709
  toolTrigger.className = 'universal-launcher-tool-trigger has-value logs-ask-ai-tool-trigger'
700
- toolTrigger.setAttribute('aria-hidden', 'true')
710
+ toolTrigger.setAttribute('aria-haspopup', 'listbox')
711
+ toolTrigger.setAttribute('aria-expanded', 'false')
701
712
  toolPicker.appendChild(toolTrigger)
702
713
 
703
714
  const toolIcon = document.createElement('span')
@@ -721,10 +732,51 @@
721
732
  toolCaret.setAttribute('aria-hidden', 'true')
722
733
  toolTrigger.appendChild(toolCaret)
723
734
 
724
- const toolSelect = document.createElement('select')
725
- toolSelect.className = 'logs-ask-ai-tool-select'
726
- toolSelect.setAttribute('aria-label', 'Agent')
727
- toolPicker.appendChild(toolSelect)
735
+ const toolSheetLayer = document.createElement('div')
736
+ toolSheetLayer.className = 'universal-launcher-tool-sheet-layer logs-ask-ai-tool-sheet-layer'
737
+ toolSheetLayer.hidden = true
738
+ panel.appendChild(toolSheetLayer)
739
+
740
+ const toolSheetBackdrop = document.createElement('button')
741
+ toolSheetBackdrop.type = 'button'
742
+ toolSheetBackdrop.className = 'universal-launcher-tool-sheet-backdrop'
743
+ toolSheetBackdrop.setAttribute('aria-label', 'Close agent selection')
744
+ toolSheetLayer.appendChild(toolSheetBackdrop)
745
+
746
+ const toolSheet = document.createElement('section')
747
+ toolSheet.className = 'universal-launcher-tool-sheet logs-ask-ai-tool-sheet'
748
+ toolSheet.setAttribute('aria-label', 'Choose agent')
749
+ toolSheetLayer.appendChild(toolSheet)
750
+
751
+ const toolSheetHeader = document.createElement('div')
752
+ toolSheetHeader.className = 'universal-launcher-tool-sheet-header'
753
+ toolSheet.appendChild(toolSheetHeader)
754
+
755
+ const toolSheetHeading = document.createElement('div')
756
+ toolSheetHeading.className = 'universal-launcher-tool-sheet-heading'
757
+ toolSheetHeader.appendChild(toolSheetHeading)
758
+
759
+ const toolSheetTitle = document.createElement('div')
760
+ toolSheetTitle.className = 'universal-launcher-tool-sheet-title'
761
+ toolSheetTitle.textContent = 'Choose agent'
762
+ toolSheetHeading.appendChild(toolSheetTitle)
763
+
764
+ const toolSheetDescription = document.createElement('div')
765
+ toolSheetDescription.className = 'universal-launcher-tool-sheet-description'
766
+ toolSheetDescription.textContent = 'Launch this report with a local agent.'
767
+ toolSheetHeading.appendChild(toolSheetDescription)
768
+
769
+ const toolSheetClose = document.createElement('button')
770
+ toolSheetClose.type = 'button'
771
+ toolSheetClose.className = 'universal-launcher-tool-sheet-close'
772
+ toolSheetClose.setAttribute('aria-label', 'Close agent selection')
773
+ toolSheetClose.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>'
774
+ toolSheetHeader.appendChild(toolSheetClose)
775
+
776
+ const toolSheetBody = document.createElement('div')
777
+ toolSheetBody.className = 'universal-launcher-tool-sheet-body logs-ask-ai-tool-sheet-body'
778
+ toolSheetBody.setAttribute('role', 'listbox')
779
+ toolSheet.appendChild(toolSheetBody)
728
780
 
729
781
  const footerActions = document.createElement('div')
730
782
  footerActions.className = 'universal-launcher-footer-actions'
@@ -737,14 +789,45 @@
737
789
  footerActions.appendChild(runButton)
738
790
 
739
791
  let returnFocusEl = null
792
+ let toolSheetOpen = false
793
+ const closeToolSheet = (options = {}) => {
794
+ toolSheetOpen = false
795
+ toolSheetLayer.hidden = true
796
+ toolPicker.classList.remove('open')
797
+ toolTrigger.setAttribute('aria-expanded', 'false')
798
+ if (options.focusTrigger !== false) {
799
+ toolTrigger.focus()
800
+ }
801
+ }
802
+ const openToolSheet = () => {
803
+ if (!Array.isArray(this.askAiTools) || this.askAiTools.length === 0) {
804
+ return
805
+ }
806
+ toolSheetOpen = true
807
+ toolSheetLayer.hidden = false
808
+ toolPicker.classList.add('open')
809
+ toolTrigger.setAttribute('aria-expanded', 'true')
810
+ window.requestAnimationFrame(() => {
811
+ const selected = toolSheetBody.querySelector('.universal-launcher-tool.selected')
812
+ const first = toolSheetBody.querySelector('.universal-launcher-tool')
813
+ const target = selected || first || toolSheetClose
814
+ if (target && typeof target.focus === 'function') {
815
+ target.focus()
816
+ }
817
+ })
818
+ }
740
819
  const syncRunState = () => {
741
820
  runButton.disabled = !String(promptTextarea.value || '').trim() || !this.selectedAskAiTool()
742
821
  }
743
822
  const getFocusable = () => {
744
- return Array.from(panel.querySelectorAll('a[href], button:not([disabled]), textarea:not([disabled]), select:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'))
823
+ const scope = toolSheetOpen ? toolSheetLayer : panel
824
+ return Array.from(scope.querySelectorAll('a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'))
745
825
  .filter((node) => node && !node.hidden && node.offsetParent !== null)
746
826
  }
747
827
  const setOpen = (isOpen) => {
828
+ if (!isOpen) {
829
+ closeToolSheet({ focusTrigger: false })
830
+ }
748
831
  overlay.hidden = !isOpen
749
832
  document.documentElement.classList.toggle('universal-launcher-open', isOpen)
750
833
  document.body.classList.toggle('universal-launcher-open', isOpen)
@@ -771,6 +854,8 @@
771
854
  const tool = this.selectedAskAiTool()
772
855
  toolLabel.textContent = tool ? tool.label : 'Choose agent'
773
856
  toolMeta.textContent = tool ? (tool.category || 'Plugin') : ''
857
+ toolTrigger.classList.toggle('has-value', Boolean(tool))
858
+ toolTrigger.setAttribute('aria-label', tool ? `Agent: ${askAiToolOptionLabel(tool)}` : 'Choose agent')
774
859
  toolIcon.textContent = ''
775
860
  while (toolIcon.firstChild) {
776
861
  toolIcon.removeChild(toolIcon.firstChild)
@@ -780,6 +865,15 @@
780
865
  icon.src = tool.iconSrc
781
866
  icon.alt = ''
782
867
  icon.className = 'logs-ask-ai-tool-trigger-image'
868
+ icon.onerror = () => {
869
+ while (toolIcon.firstChild) {
870
+ toolIcon.removeChild(toolIcon.firstChild)
871
+ }
872
+ const fallbackIcon = document.createElement('i')
873
+ fallbackIcon.className = 'fa-solid fa-robot'
874
+ fallbackIcon.setAttribute('aria-hidden', 'true')
875
+ toolIcon.appendChild(fallbackIcon)
876
+ }
783
877
  toolIcon.appendChild(icon)
784
878
  } else {
785
879
  const icon = document.createElement('i')
@@ -787,6 +881,11 @@
787
881
  icon.setAttribute('aria-hidden', 'true')
788
882
  toolIcon.appendChild(icon)
789
883
  }
884
+ ;(this.askAiLauncher && this.askAiLauncher.toolEntries ? this.askAiLauncher.toolEntries : []).forEach((entry) => {
885
+ const selected = Boolean(tool && entry.tool === tool)
886
+ entry.button.classList.toggle('selected', selected)
887
+ entry.button.setAttribute('aria-selected', selected ? 'true' : 'false')
888
+ })
790
889
  syncRunState()
791
890
  }
792
891
  const run = async () => {
@@ -799,7 +898,7 @@
799
898
  }
800
899
  if (!tool) {
801
900
  error.textContent = 'Choose an agent.'
802
- toolSelect.focus()
901
+ toolTrigger.focus()
803
902
  return
804
903
  }
805
904
  error.textContent = ''
@@ -813,23 +912,31 @@
813
912
  }
814
913
 
815
914
  closeButton.addEventListener('click', close)
915
+ toolTrigger.addEventListener('click', () => {
916
+ if (toolSheetOpen) {
917
+ closeToolSheet()
918
+ } else {
919
+ openToolSheet()
920
+ }
921
+ })
922
+ toolSheetBackdrop.addEventListener('click', () => closeToolSheet())
923
+ toolSheetClose.addEventListener('click', () => closeToolSheet())
816
924
  overlay.addEventListener('click', (event) => {
817
925
  if (event.target === overlay) {
818
926
  close()
819
927
  }
820
928
  })
821
- toolSelect.addEventListener('change', syncTool)
822
- toolSelect.addEventListener('change', () => {
823
- const tool = this.selectedAskAiTool()
824
- setStoredToolPreference(tool && tool.value ? tool.value : '')
825
- })
826
929
  promptTextarea.addEventListener('input', syncRunState)
827
930
  runButton.addEventListener('click', run)
828
931
  overlay.addEventListener('keydown', (event) => {
829
932
  event.stopPropagation()
830
933
  if (event.key === 'Escape') {
831
934
  event.preventDefault()
832
- close()
935
+ if (toolSheetOpen) {
936
+ closeToolSheet()
937
+ } else {
938
+ close()
939
+ }
833
940
  } else if (event.key === 'Tab') {
834
941
  const focusable = getFocusable()
835
942
  if (focusable.length === 0) {
@@ -858,38 +965,131 @@
858
965
  overlay,
859
966
  panel,
860
967
  promptTextarea,
861
- toolSelect,
968
+ toolTrigger,
969
+ toolSheetBody,
970
+ toolSheetLayer,
862
971
  toolLabel,
863
972
  toolMeta,
864
973
  toolIcon,
974
+ selectedToolIndex: -1,
975
+ toolEntries: [],
976
+ setToolIndex(index) {
977
+ const nextIndex = Number.isFinite(index) ? index : -1
978
+ this.selectedToolIndex = nextIndex
979
+ const tool = Array.isArray(this.owner.askAiTools) ? this.owner.askAiTools[nextIndex] : null
980
+ setStoredToolPreference(tool && tool.value ? tool.value : '')
981
+ this.syncTool()
982
+ },
865
983
  error,
866
984
  runButton,
867
985
  setOpen,
986
+ closeToolSheet,
868
987
  syncTool,
869
- syncRunState
988
+ syncRunState,
989
+ owner: this
870
990
  }
871
991
  return this.askAiLauncher
872
992
  }
873
993
  populateAskAiLauncherTools(tools) {
874
994
  const launcher = this.createAskAiLauncher()
875
- while (launcher.toolSelect.firstChild) {
876
- launcher.toolSelect.removeChild(launcher.toolSelect.firstChild)
995
+ while (launcher.toolSheetBody.firstChild) {
996
+ launcher.toolSheetBody.removeChild(launcher.toolSheetBody.firstChild)
997
+ }
998
+ launcher.toolEntries = []
999
+ const createFallbackToolIcon = () => {
1000
+ const icon = document.createElement('span')
1001
+ icon.className = 'universal-launcher-tool-icon logs-ask-ai-tool-fallback-icon'
1002
+ icon.innerHTML = '<i class="fa-solid fa-robot" aria-hidden="true"></i>'
1003
+ return icon
877
1004
  }
878
- const placeholder = document.createElement('option')
879
- placeholder.value = ''
880
- placeholder.textContent = 'Choose agent'
881
- launcher.toolSelect.appendChild(placeholder)
1005
+ const groups = new Map()
882
1006
  tools.forEach((tool, index) => {
883
- const option = document.createElement('option')
884
- option.value = String(index)
885
- option.textContent = askAiToolOptionLabel(tool)
886
- launcher.toolSelect.appendChild(option)
1007
+ const category = tool && tool.category ? tool.category : 'Plugin'
1008
+ if (!groups.has(category)) {
1009
+ groups.set(category, [])
1010
+ }
1011
+ groups.get(category).push({ tool, index })
1012
+ })
1013
+ const orderedGroupCategories = []
1014
+ ASK_AI_TOOL_CATEGORY_ORDER.forEach((preferredCategory) => {
1015
+ const match = Array.from(groups.keys()).find((category) => String(category).toLowerCase() === preferredCategory.toLowerCase())
1016
+ if (match && !orderedGroupCategories.includes(match)) {
1017
+ orderedGroupCategories.push(match)
1018
+ }
1019
+ })
1020
+ Array.from(groups.keys()).sort((a, b) => {
1021
+ const rankDelta = askAiToolCategoryRank(a) - askAiToolCategoryRank(b)
1022
+ if (rankDelta !== 0) return rankDelta
1023
+ return String(a || '').localeCompare(String(b || ''), undefined, { sensitivity: 'base' })
1024
+ }).forEach((category) => {
1025
+ if (!orderedGroupCategories.includes(category)) {
1026
+ orderedGroupCategories.push(category)
1027
+ }
1028
+ })
1029
+ orderedGroupCategories.forEach((category) => {
1030
+ const entries = groups.get(category) || []
1031
+ const group = document.createElement('div')
1032
+ group.className = 'universal-launcher-tool-group'
1033
+ const heading = document.createElement('div')
1034
+ heading.className = 'universal-launcher-tool-group-title'
1035
+ heading.textContent = category
1036
+ group.appendChild(heading)
1037
+ const list = document.createElement('div')
1038
+ list.className = 'universal-launcher-tool-list logs-ask-ai-tool-list'
1039
+ group.appendChild(list)
1040
+ entries.forEach(({ tool, index }) => {
1041
+ const option = document.createElement('button')
1042
+ option.type = 'button'
1043
+ option.className = 'universal-launcher-tool'
1044
+ option.setAttribute('role', 'option')
1045
+ option.setAttribute('aria-selected', 'false')
1046
+ option.dataset.toolIndex = String(index)
1047
+
1048
+ const indicator = document.createElement('span')
1049
+ indicator.className = 'universal-launcher-tool-indicator'
1050
+ indicator.setAttribute('aria-hidden', 'true')
1051
+ option.appendChild(indicator)
1052
+
1053
+ if (tool && tool.iconSrc) {
1054
+ const icon = document.createElement('img')
1055
+ icon.className = 'universal-launcher-tool-icon'
1056
+ icon.src = tool.iconSrc
1057
+ icon.alt = ''
1058
+ icon.onerror = () => {
1059
+ icon.replaceWith(createFallbackToolIcon())
1060
+ }
1061
+ option.appendChild(icon)
1062
+ } else {
1063
+ option.appendChild(createFallbackToolIcon())
1064
+ }
1065
+
1066
+ const text = document.createElement('span')
1067
+ text.className = 'universal-launcher-tool-copy'
1068
+ const label = document.createElement('span')
1069
+ label.className = 'universal-launcher-tool-label'
1070
+ label.textContent = tool ? tool.label : 'Agent'
1071
+ text.appendChild(label)
1072
+ const meta = document.createElement('span')
1073
+ meta.className = 'universal-launcher-tool-meta'
1074
+ meta.textContent = tool && tool.category ? tool.category : 'Plugin'
1075
+ text.appendChild(meta)
1076
+ option.appendChild(text)
1077
+
1078
+ option.addEventListener('click', () => {
1079
+ launcher.setToolIndex(index)
1080
+ launcher.closeToolSheet()
1081
+ launcher.toolTrigger.focus()
1082
+ })
1083
+ list.appendChild(option)
1084
+ launcher.toolEntries.push({ button: option, tool })
1085
+ })
1086
+ launcher.toolSheetBody.appendChild(group)
887
1087
  })
888
1088
  const preferredTool = getStoredToolPreference()
889
1089
  const preferredIndex = preferredTool
890
1090
  ? tools.findIndex((tool) => tool && tool.value === preferredTool)
891
1091
  : -1
892
- launcher.toolSelect.value = preferredIndex >= 0 ? String(preferredIndex) : ''
1092
+ launcher.selectedToolIndex = preferredIndex >= 0 ? preferredIndex : -1
893
1093
  if (preferredTool && preferredIndex < 0) {
894
1094
  setStoredToolPreference('')
895
1095
  }
@@ -900,8 +1100,8 @@
900
1100
  if (!launcher || !Array.isArray(this.askAiTools)) {
901
1101
  return null
902
1102
  }
903
- const index = Number.parseInt(launcher.toolSelect.value, 10)
904
- return Number.isFinite(index) ? this.askAiTools[index] || null : null
1103
+ const index = launcher.selectedToolIndex
1104
+ return Number.isFinite(index) && index >= 0 ? this.askAiTools[index] || null : null
905
1105
  }
906
1106
  async openAskAiModal() {
907
1107
  if (!this.currentMarkdown && !this.reviewMarkdown) {
@@ -5298,25 +5298,33 @@ body.dark .logs-help-action--ai:focus-visible {
5298
5298
  width: 100%;
5299
5299
  }
5300
5300
  .logs-ask-ai-tool-trigger {
5301
- pointer-events: none;
5301
+ pointer-events: auto;
5302
+ font: inherit;
5302
5303
  }
5303
5304
  .logs-ask-ai-tool-picker:focus-within .logs-ask-ai-tool-trigger {
5304
5305
  border-color: var(--universal-launcher-border-strong);
5305
5306
  background: var(--universal-launcher-control-surface);
5306
5307
  outline: none;
5307
5308
  }
5308
- .logs-ask-ai-tool-select {
5309
- position: absolute;
5310
- inset: 0;
5311
- width: 100%;
5312
- height: 100%;
5313
- appearance: none;
5314
- border: 0;
5315
- opacity: 0;
5316
- cursor: pointer;
5309
+ .logs-ask-ai-tool-sheet-layer {
5310
+ padding: 12px;
5317
5311
  }
5318
- .logs-ask-ai-tool-select:disabled {
5319
- cursor: default;
5312
+ .logs-ask-ai-tool-sheet {
5313
+ width: min(100%, 520px);
5314
+ max-height: 100%;
5315
+ }
5316
+ .logs-ask-ai-tool-sheet.logs-ask-ai-tool-sheet .universal-launcher-tool-meta {
5317
+ display: block;
5318
+ }
5319
+ .logs-ask-ai-tool-list {
5320
+ grid-template-columns: repeat(2, minmax(0, 1fr));
5321
+ }
5322
+ .logs-ask-ai-tool-fallback-icon {
5323
+ display: inline-flex;
5324
+ align-items: center;
5325
+ justify-content: center;
5326
+ color: var(--universal-launcher-text-muted);
5327
+ font-size: 13px;
5320
5328
  }
5321
5329
  .logs-ask-ai-tool-trigger-icon {
5322
5330
  display: inline-flex;
@@ -5332,6 +5340,14 @@ body.dark .logs-help-action--ai:focus-visible {
5332
5340
  object-fit: contain;
5333
5341
  border-radius: 4px;
5334
5342
  }
5343
+ @media (max-width: 560px), (max-height: 520px) {
5344
+ .logs-ask-ai-tool-list {
5345
+ grid-template-columns: minmax(0, 1fr);
5346
+ }
5347
+ .logs-ask-ai-tool-sheet-layer {
5348
+ padding: 8px;
5349
+ }
5350
+ }
5335
5351
  .logs-report-pane,
5336
5352
  .logs-redaction-pane {
5337
5353
  min-width: 0;
@@ -820,6 +820,13 @@ body.dark .home-app-line {
820
820
  border-radius: 6px;
821
821
  object-fit: cover;
822
822
  }
823
+ .home-app-line .description {
824
+ margin-bottom: 0;
825
+ line-height: 1.32;
826
+ }
827
+ .home-app-line .menu-btns {
828
+ margin-top: 4px;
829
+ }
823
830
  .menu-btns .home-icon-btn {
824
831
  width: 28px;
825
832
  height: 22px;
@@ -0,0 +1,169 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
4
+ const test = require('node:test')
5
+
6
+ const system = require('systeminformation')
7
+
8
+ const amd = require('../kernel/gpu/amd')
9
+ const gfxTargets = require('../kernel/gpu/amd_gfx_targets.json')
10
+ const packageJson = require('../package.json')
11
+ const Sysinfo = require('../kernel/sysinfo')
12
+
13
+ const root = path.join(__dirname, '..')
14
+
15
+ test('AMD gfx generated data is checked in and self describing', () => {
16
+ assert.equal(
17
+ gfxTargets.source,
18
+ 'https://raw.githubusercontent.com/ROCm/ROCm/develop/docs/reference/gpu-arch-specs.rst'
19
+ )
20
+ assert.equal(gfxTargets.generated_by, 'script/update-amd-gfx-targets.js')
21
+
22
+ assert.equal(gfxTargets.entries['radeon rx 6800 xt'], 'gfx1030')
23
+ assert.equal(gfxTargets.entries['radeon rx 7900 xtx'], 'gfx1100')
24
+ assert.equal(gfxTargets.entries.mi210, 'gfx90a')
25
+ })
26
+
27
+ test('AMD gfx target refresh is an explicit maintainer command', () => {
28
+ assert.equal(packageJson.scripts['update:amd-gfx-targets'], 'node script/update-amd-gfx-targets.js')
29
+ assert.equal(Object.prototype.hasOwnProperty.call(packageJson.scripts, 'update:amd-rocm-targets'), false)
30
+
31
+ for (const script of ['preinstall', 'install', 'postinstall', 'prestart', 'start', 'poststart']) {
32
+ assert.doesNotMatch(packageJson.scripts[script] || '', /update-amd-gfx-targets/)
33
+ }
34
+ })
35
+
36
+ test('AMD gfx resolver maps common product names to exact gfx targets', () => {
37
+ const cases = [
38
+ ['AMD Radeon RX 6800 XT', 'gfx1030'],
39
+ ['AMD Radeon RX 7600', 'gfx1102'],
40
+ ['Advanced Micro Devices Radeon RX 7900 XTX', 'gfx1100'],
41
+ ['AMD Radeon RX 9070 XT', 'gfx1201'],
42
+ ['AMD Radeon AI PRO R9700', 'gfx1201'],
43
+ ['AMD Instinct MI210', 'gfx90a'],
44
+ ['AMD Radeon 780M', 'gfx1103'],
45
+ ['AMD Radeon 890M', 'gfx1150'],
46
+ ['gfx1030', 'gfx1030']
47
+ ]
48
+
49
+ for (const [model, target] of cases) {
50
+ assert.equal(amd.resolve_rocm_gfx_target(model), target, model)
51
+ }
52
+ })
53
+
54
+ test('AMD gfx resolver rejects unknown products and preserves raw gfx targets', () => {
55
+ const unknownModels = [
56
+ 'AMD Radeon RX 480',
57
+ 'AMD Radeon RX 5700 XT',
58
+ 'AMD Radeon RX 9999',
59
+ 'AMD Radeon RX 9999M XT'
60
+ ]
61
+
62
+ for (const model of unknownModels) {
63
+ assert.equal(amd.resolve_rocm_gfx_target(model), null, model)
64
+ }
65
+
66
+ for (const target of ['gfx803', 'gfx1013', 'gfx1104', 'gfx1209', 'GFX90A']) {
67
+ assert.equal(amd.resolve_rocm_gfx_target(target), target.toLowerCase(), target)
68
+ }
69
+ })
70
+
71
+ test('AMD gpu_target resolution uses CPU brand only for generic Radeon Graphics models', async () => {
72
+ let cpuBrandCalls = 0
73
+ const cpuBrand = async () => {
74
+ cpuBrandCalls += 1
75
+ return 'AMD Ryzen AI 9 HX 375'
76
+ }
77
+
78
+ assert.equal(await amd.resolve_gpu_target('AMD Radeon RX 6800 XT', cpuBrand), 'gfx1030')
79
+ assert.equal(cpuBrandCalls, 0)
80
+
81
+ assert.equal(await amd.resolve_gpu_target('AMD Radeon RX 480', cpuBrand), null)
82
+ assert.equal(cpuBrandCalls, 0)
83
+
84
+ assert.equal(await amd.resolve_gpu_target('AMD Radeon Graphics', cpuBrand), 'gfx1150')
85
+ assert.equal(cpuBrandCalls, 1)
86
+
87
+ assert.equal(await amd.resolve_gpu_target('AMD Radeon Graphics', 'Unknown AMD CPU'), null)
88
+ })
89
+
90
+ test('Sysinfo exposes AMD gpu_target for resolved discrete GPUs', async (t) => {
91
+ t.mock.method(system, 'graphics', async () => ({
92
+ controllers: [
93
+ {
94
+ vendor: 'AMD',
95
+ model: 'AMD Radeon RX 6800 XT',
96
+ vram: 16384,
97
+ driverVersion: '31.0.1'
98
+ }
99
+ ],
100
+ displays: []
101
+ }))
102
+
103
+ const sys = new Sysinfo()
104
+ sys.info = {}
105
+
106
+ await sys.gpus()
107
+
108
+ assert.equal(sys.info.gpu, 'amd')
109
+ assert.equal(sys.info.gpu_target, 'gfx1030')
110
+ })
111
+
112
+ test('Sysinfo leaves unresolved AMD gpu_target null', async (t) => {
113
+ t.mock.method(system, 'graphics', async () => ({
114
+ controllers: [
115
+ {
116
+ vendor: 'AMD',
117
+ model: 'AMD Radeon RX 480',
118
+ vram: 8192,
119
+ driverVersion: '31.0.1'
120
+ }
121
+ ],
122
+ displays: []
123
+ }))
124
+
125
+ const sys = new Sysinfo()
126
+ sys.info = {}
127
+
128
+ await sys.gpus()
129
+
130
+ assert.equal(sys.info.gpu, 'amd')
131
+ assert.equal(sys.info.gpu_target, null)
132
+ })
133
+
134
+ test('Sysinfo exposes AMD gpu_target through generic APU CPU fallback', async (t) => {
135
+ let cpuBrandCalls = 0
136
+ t.mock.method(system, 'graphics', async () => ({
137
+ controllers: [
138
+ {
139
+ vendor: 'AMD',
140
+ model: 'AMD Radeon Graphics',
141
+ vram: 16384,
142
+ driverVersion: '31.0.1'
143
+ }
144
+ ],
145
+ displays: []
146
+ }))
147
+ t.mock.method(system, 'cpu', async () => {
148
+ cpuBrandCalls += 1
149
+ return { brand: 'AMD Ryzen AI 9 HX 375' }
150
+ })
151
+
152
+ const sys = new Sysinfo()
153
+ sys.info = {}
154
+
155
+ await sys.gpus()
156
+
157
+ assert.equal(sys.info.gpu, 'amd')
158
+ assert.equal(sys.info.gpu_target, 'gfx1150')
159
+ assert.equal(cpuBrandCalls, 1)
160
+ })
161
+
162
+ test('AMD gfx runtime resolver stays offline and data-only', () => {
163
+ const source = fs.readFileSync(path.join(root, 'kernel', 'gpu', 'amd.js'), 'utf8')
164
+
165
+ assert.doesNotMatch(source, /require\(["'](?:node:)?https?["']\)/)
166
+ assert.doesNotMatch(source, /require\(["'](?:node:)?child_process["']\)/)
167
+ assert.doesNotMatch(source, /\b(fetch|axios|rocminfo|hipInfo|ze_info)\b/)
168
+ assert.doesNotMatch(source, /\b(kfd|topology)\b/i)
169
+ })