factory-ai 2.0.0 → 2.1.0
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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/cmd/factory-ui/main.go +212 -68
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes follow semantic versioning and the Keep a Changelog structur
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.0] - 2026-07-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Redesigned the native TUI around a persistent workspace selector with workspace-scoped Dashboard, Objectives, and Agents pages.
|
|
12
|
+
- Consolidated secrets, logs, models, runtime, and capabilities under Settings.
|
|
13
|
+
- Added dedicated bounded command-console and always-visible command-line panes with scrollback, clear controls, history, inline errors, and selected-workspace submit shortcuts.
|
|
14
|
+
|
|
7
15
|
## [2.0.0] - 2026-07-13
|
|
8
16
|
|
|
9
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ factory ui
|
|
|
96
96
|
| `factory secret set NAME` | Store a credential in global Key Vault |
|
|
97
97
|
| `factory github connect ORG` | Connect GitHub Enterprise credentials |
|
|
98
98
|
|
|
99
|
-
Inside `factory ui`,
|
|
99
|
+
Inside `factory ui`, select a workspace from the persistent left sidebar with `[`/`]`; Dashboard, Objectives, and Agents are scoped to it. Settings contains secrets, logs, models, runtime, and capabilities. Press `:` or Enter to focus the dedicated command line and enter any Factory command without the `factory` prefix. Use `↑/↓` for command history and `Ctrl+↑/↓` for console scrollback. Shortcuts: `n` submit to the selected workspace, `i` import, `a` set secret, `y/x` approve or deny, `p/u` pause or resume, `?` help, `Ctrl+L` clear console, and `q` quit.
|
|
100
100
|
|
|
101
101
|
## Architecture
|
|
102
102
|
|
package/cmd/factory-ui/main.go
CHANGED
|
@@ -10,6 +10,7 @@ import (
|
|
|
10
10
|
"os/exec"
|
|
11
11
|
"path/filepath"
|
|
12
12
|
"regexp"
|
|
13
|
+
"sort"
|
|
13
14
|
"strconv"
|
|
14
15
|
"strings"
|
|
15
16
|
"sync"
|
|
@@ -110,24 +111,26 @@ type commandResultMsg struct {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
type model struct {
|
|
113
|
-
client
|
|
114
|
-
factoryName
|
|
115
|
-
purpose
|
|
116
|
-
width
|
|
117
|
-
height
|
|
118
|
-
tab
|
|
119
|
-
scroll
|
|
120
|
-
dashboard
|
|
121
|
-
logs
|
|
122
|
-
workspaces
|
|
123
|
-
workspaceErr
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
client *azblob.Client
|
|
115
|
+
factoryName string
|
|
116
|
+
purpose string
|
|
117
|
+
width int
|
|
118
|
+
height int
|
|
119
|
+
tab int
|
|
120
|
+
scroll int
|
|
121
|
+
dashboard dashboard
|
|
122
|
+
logs string
|
|
123
|
+
workspaces []workspace
|
|
124
|
+
workspaceErr string
|
|
125
|
+
selectedWorkspace string
|
|
126
|
+
loading bool
|
|
127
|
+
err error
|
|
128
|
+
commandMode bool
|
|
129
|
+
commandInput string
|
|
130
|
+
commandOutput string
|
|
131
|
+
commandHistory []string
|
|
132
|
+
historyIndex int
|
|
133
|
+
consoleScroll int
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
var (
|
|
@@ -138,7 +141,7 @@ var (
|
|
|
138
141
|
muted = lipgloss.Color("#7F8B99")
|
|
139
142
|
panel = lipgloss.Color("#15191F")
|
|
140
143
|
border = lipgloss.Color("#303743")
|
|
141
|
-
tabs = []string{"
|
|
144
|
+
tabs = []string{"Dashboard", "Objectives", "Agents", "Settings"}
|
|
142
145
|
ansi = regexp.MustCompile(`\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?)`)
|
|
143
146
|
)
|
|
144
147
|
|
|
@@ -406,13 +409,32 @@ func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
406
409
|
m.scroll += 10
|
|
407
410
|
case "home":
|
|
408
411
|
m.scroll = 0
|
|
409
|
-
case "
|
|
412
|
+
case "[", "shift+up":
|
|
413
|
+
if index := m.selectedIndex(); index > 0 {
|
|
414
|
+
m.selectedWorkspace = m.workspaces[index-1].Name
|
|
415
|
+
m.scroll = 0
|
|
416
|
+
}
|
|
417
|
+
case "]", "shift+down":
|
|
418
|
+
if index := m.selectedIndex(); index >= 0 && index < len(m.workspaces)-1 {
|
|
419
|
+
m.selectedWorkspace = m.workspaces[index+1].Name
|
|
420
|
+
m.scroll = 0
|
|
421
|
+
}
|
|
422
|
+
case "ctrl+up":
|
|
423
|
+
if m.consoleScroll > 0 {
|
|
424
|
+
m.consoleScroll--
|
|
425
|
+
}
|
|
426
|
+
case "ctrl+down":
|
|
427
|
+
m.consoleScroll++
|
|
428
|
+
case ":", "/", "enter", "o":
|
|
410
429
|
m.commandMode = true
|
|
411
430
|
m.commandInput = ""
|
|
412
431
|
m.historyIndex = len(m.commandHistory)
|
|
413
432
|
case "n":
|
|
414
433
|
m.commandMode = true
|
|
415
434
|
m.commandInput = "submit "
|
|
435
|
+
if selected := m.selected(); selected != nil {
|
|
436
|
+
m.commandInput += selected.Name + " "
|
|
437
|
+
}
|
|
416
438
|
m.historyIndex = len(m.commandHistory)
|
|
417
439
|
case "i":
|
|
418
440
|
m.commandMode = true
|
|
@@ -439,11 +461,15 @@ func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
439
461
|
case "?":
|
|
440
462
|
m.loading = true
|
|
441
463
|
return m, executeFactoryCommand([]string{"--help"})
|
|
464
|
+
case "ctrl+l", "esc":
|
|
465
|
+
m.commandOutput = ""
|
|
466
|
+
m.consoleScroll = 0
|
|
467
|
+
m.err = nil
|
|
442
468
|
case "r":
|
|
443
469
|
m.loading = true
|
|
444
470
|
return m, fetchCmd(m.client)
|
|
445
471
|
default:
|
|
446
|
-
if len(msg.String()) == 1 && msg.String()[0] >= '1' && msg.String()[0] <= '
|
|
472
|
+
if len(msg.String()) == 1 && msg.String()[0] >= '1' && msg.String()[0] <= '4' {
|
|
447
473
|
m.tab = int(msg.String()[0] - '1')
|
|
448
474
|
m.scroll = 0
|
|
449
475
|
}
|
|
@@ -455,16 +481,24 @@ func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
455
481
|
m.err = msg.err
|
|
456
482
|
if msg.err == nil {
|
|
457
483
|
m.dashboard, m.logs, m.workspaces, m.workspaceErr = msg.dashboard, msg.logs, msg.workspaces, msg.workspaceErr
|
|
484
|
+
if len(m.workspaces) == 0 {
|
|
485
|
+
m.selectedWorkspace = ""
|
|
486
|
+
} else if m.selected() == nil {
|
|
487
|
+
m.selectedWorkspace = m.workspaces[0].Name
|
|
488
|
+
}
|
|
458
489
|
}
|
|
459
490
|
case commandResultMsg:
|
|
460
491
|
m.loading = false
|
|
461
|
-
m.err =
|
|
492
|
+
m.err = nil
|
|
462
493
|
result := strings.TrimSpace(clean(msg.output))
|
|
463
494
|
if msg.err != nil && result == "" {
|
|
464
495
|
result = msg.err.Error()
|
|
465
496
|
}
|
|
466
497
|
m.commandOutput = fmt.Sprintf("$ %s\n%s", clean(msg.command), result)
|
|
467
|
-
m.
|
|
498
|
+
m.consoleScroll = len(strings.Split(m.commandOutput, "\n")) - 4
|
|
499
|
+
if m.consoleScroll < 0 {
|
|
500
|
+
m.consoleScroll = 0
|
|
501
|
+
}
|
|
468
502
|
return m, fetchCmd(m.client)
|
|
469
503
|
case tickMsg:
|
|
470
504
|
if !m.loading {
|
|
@@ -499,15 +533,58 @@ func trim(value string, max int) string {
|
|
|
499
533
|
return value
|
|
500
534
|
}
|
|
501
535
|
|
|
536
|
+
func normalizeRepository(value string) string {
|
|
537
|
+
return strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(value), "https://github.com/"), ".git")
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
func (m model) selected() *workspace {
|
|
541
|
+
for index := range m.workspaces {
|
|
542
|
+
if m.workspaces[index].Name == m.selectedWorkspace {
|
|
543
|
+
return &m.workspaces[index]
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return nil
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
func (m model) selectedIndex() int {
|
|
550
|
+
for index, item := range m.workspaces {
|
|
551
|
+
if item.Name == m.selectedWorkspace {
|
|
552
|
+
return index
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return -1
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
func (m model) visibleObjectives() []objective {
|
|
559
|
+
selected := m.selected()
|
|
560
|
+
if selected == nil {
|
|
561
|
+
return nil
|
|
562
|
+
}
|
|
563
|
+
repository := normalizeRepository(selected.Repository)
|
|
564
|
+
result := []objective{}
|
|
565
|
+
for _, item := range m.dashboard.Objectives {
|
|
566
|
+
if normalizeRepository(item.Repository) == repository {
|
|
567
|
+
result = append(result, item)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return result
|
|
571
|
+
}
|
|
572
|
+
|
|
502
573
|
func (m model) overview() string {
|
|
503
574
|
cost := "unavailable"
|
|
504
575
|
if m.dashboard.Cost != nil {
|
|
505
576
|
cost = fmt.Sprintf("%s %.2f", m.dashboard.Cost.Currency, m.dashboard.Cost.MonthToDate)
|
|
506
577
|
}
|
|
578
|
+
objectives := m.visibleObjectives()
|
|
579
|
+
countMap := map[string]int{}
|
|
580
|
+
for _, item := range objectives {
|
|
581
|
+
countMap[item.Status]++
|
|
582
|
+
}
|
|
507
583
|
counts := []string{}
|
|
508
|
-
for key, value := range
|
|
584
|
+
for key, value := range countMap {
|
|
509
585
|
counts = append(counts, fmt.Sprintf("%s %d", key, value))
|
|
510
586
|
}
|
|
587
|
+
sort.Strings(counts)
|
|
511
588
|
in, cached, out := 0, 0, 0
|
|
512
589
|
for _, value := range m.dashboard.ModelUsage {
|
|
513
590
|
in += value.InputTokens
|
|
@@ -515,8 +592,8 @@ func (m model) overview() string {
|
|
|
515
592
|
out += value.OutputTokens
|
|
516
593
|
}
|
|
517
594
|
value := fmt.Sprintf("%s\n\nHealth %s\nQueue %s\nDead letters %d\nAzure MTD %s\nObjectives %s\nTokens %d in · %d cached · %d out\n\n%s\n\n", lipgloss.NewStyle().Bold(true).Render("SYSTEM"), badge(m.dashboard.Health.Status), lipgloss.NewStyle().Foreground(accent).Render(fmt.Sprint(m.dashboard.Queue.Active)), m.dashboard.Queue.DeadLetter, lipgloss.NewStyle().Foreground(warn).Render(cost), strings.Join(counts, " "), in, cached, out, lipgloss.NewStyle().Bold(true).Render("RECENT OBJECTIVES"))
|
|
518
|
-
for index := len(
|
|
519
|
-
item :=
|
|
595
|
+
for index := len(objectives) - 1; index >= 0 && index >= len(objectives)-8; index-- {
|
|
596
|
+
item := objectives[index]
|
|
520
597
|
value += fmt.Sprintf("%s %s\n %s\n\n", badge(item.Status), trim(item.Objective, 90), lipgloss.NewStyle().Foreground(muted).Render(clean(item.Repository)))
|
|
521
598
|
}
|
|
522
599
|
return value
|
|
@@ -524,8 +601,9 @@ func (m model) overview() string {
|
|
|
524
601
|
|
|
525
602
|
func (m model) objectives() string {
|
|
526
603
|
var value strings.Builder
|
|
527
|
-
|
|
528
|
-
|
|
604
|
+
objectives := m.visibleObjectives()
|
|
605
|
+
for index := len(objectives) - 1; index >= 0; index-- {
|
|
606
|
+
item := objectives[index]
|
|
529
607
|
fmt.Fprintf(&value, "%s %s\n%s\n", badge(item.Status), trim(item.Objective, 100), lipgloss.NewStyle().Foreground(muted).Render(clean(item.ID)))
|
|
530
608
|
for _, task := range item.Tasks {
|
|
531
609
|
state := task.State
|
|
@@ -554,7 +632,7 @@ func (m model) objectives() string {
|
|
|
554
632
|
|
|
555
633
|
func (m model) agents() string {
|
|
556
634
|
var value strings.Builder
|
|
557
|
-
for _, item := range m.
|
|
635
|
+
for _, item := range m.visibleObjectives() {
|
|
558
636
|
for _, task := range item.Tasks {
|
|
559
637
|
if task.State == "succeeded" {
|
|
560
638
|
continue
|
|
@@ -586,47 +664,80 @@ func (m model) agents() string {
|
|
|
586
664
|
return value.String()
|
|
587
665
|
}
|
|
588
666
|
|
|
589
|
-
func (m model)
|
|
667
|
+
func (m model) sidebar(maxLines int) string {
|
|
668
|
+
var value strings.Builder
|
|
669
|
+
value.WriteString(lipgloss.NewStyle().Bold(true).Render("WORKSPACES") + "\n\n")
|
|
590
670
|
if m.workspaceErr != "" {
|
|
591
|
-
|
|
671
|
+
value.WriteString(lipgloss.NewStyle().Foreground(danger).Render("Catalog unavailable") + "\n")
|
|
592
672
|
}
|
|
593
673
|
if len(m.workspaces) == 0 {
|
|
594
|
-
|
|
674
|
+
value.WriteString("No workspaces\n\nPress i to import")
|
|
595
675
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
676
|
+
available := maxLines - 7
|
|
677
|
+
if available < 1 {
|
|
678
|
+
available = 1
|
|
679
|
+
}
|
|
680
|
+
start := 0
|
|
681
|
+
if index := m.selectedIndex(); index >= available {
|
|
682
|
+
start = index - available + 1
|
|
683
|
+
}
|
|
684
|
+
end := start + available
|
|
685
|
+
if end > len(m.workspaces) {
|
|
686
|
+
end = len(m.workspaces)
|
|
687
|
+
}
|
|
688
|
+
if start > 0 {
|
|
689
|
+
value.WriteString(lipgloss.NewStyle().Foreground(muted).Render(" ↑ more") + "\n")
|
|
690
|
+
}
|
|
691
|
+
for _, item := range m.workspaces[start:end] {
|
|
692
|
+
marker := " "
|
|
693
|
+
style := lipgloss.NewStyle().Foreground(muted)
|
|
694
|
+
if item.Name == m.selectedWorkspace {
|
|
695
|
+
marker = "› "
|
|
696
|
+
style = style.Foreground(accent).Bold(true)
|
|
697
|
+
}
|
|
698
|
+
value.WriteString(style.Render(marker+trim(item.Name, 20)) + "\n")
|
|
699
|
+
}
|
|
700
|
+
if end < len(m.workspaces) {
|
|
701
|
+
value.WriteString(lipgloss.NewStyle().Foreground(muted).Render(" ↓ more") + "\n")
|
|
600
702
|
}
|
|
703
|
+
value.WriteString("\n" + lipgloss.NewStyle().Foreground(muted).Render("[ / ] select\ni import\nn new objective"))
|
|
704
|
+
return value.String()
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
func (m model) settings() string {
|
|
708
|
+
var value strings.Builder
|
|
709
|
+
value.WriteString("RUNTIME\n")
|
|
710
|
+
fmt.Fprintf(&value, " Name %s\n Purpose %s\n Storage Azure Blob\n Refresh 15 seconds\n\n", clean(m.factoryName), clean(m.purpose))
|
|
711
|
+
value.WriteString("MODELS\n :models show\n :models set ROLE PROVIDER/MODEL\n\nSECRETS (values hidden)\n")
|
|
712
|
+
for _, item := range m.dashboard.Secrets {
|
|
713
|
+
fmt.Fprintf(&value, " ● %-36s %s\n", clean(item.Name), clean(item.Updated))
|
|
714
|
+
}
|
|
715
|
+
if len(m.dashboard.Secrets) == 0 {
|
|
716
|
+
value.WriteString(" No secret metadata available\n")
|
|
717
|
+
}
|
|
718
|
+
value.WriteString("\nCAPABILITIES\n Skills, MCP servers, scanners, ACP, and signed extensions\n :extension verify MANIFEST ARTIFACT PUBLIC_KEY\n")
|
|
719
|
+
value.WriteString("\nRECENT SERVICE LOGS\n")
|
|
720
|
+
lines := strings.Split(clean(m.logs), "\n")
|
|
721
|
+
if len(lines) > 30 {
|
|
722
|
+
lines = lines[len(lines)-30:]
|
|
723
|
+
}
|
|
724
|
+
value.WriteString(strings.Join(lines, "\n"))
|
|
601
725
|
return value.String()
|
|
602
726
|
}
|
|
603
727
|
|
|
604
728
|
func (m model) body() string {
|
|
729
|
+
if m.tab < 3 && m.selected() == nil {
|
|
730
|
+
return "SELECT A WORKSPACE\n\nImport one with i, then select it from the left sidebar.\nObjectives and agents are scoped to the selected workspace."
|
|
731
|
+
}
|
|
605
732
|
switch m.tab {
|
|
606
733
|
case 0:
|
|
607
734
|
return m.overview()
|
|
608
735
|
case 1:
|
|
609
|
-
return m.workspaceList()
|
|
610
|
-
case 2:
|
|
611
736
|
return m.objectives()
|
|
612
|
-
case
|
|
737
|
+
case 2:
|
|
613
738
|
return m.agents()
|
|
614
|
-
case 4:
|
|
615
|
-
var b strings.Builder
|
|
616
|
-
b.WriteString("GLOBAL KEY VAULT\nValues are never displayed.\n\n")
|
|
617
|
-
for _, item := range m.dashboard.Secrets {
|
|
618
|
-
fmt.Fprintf(&b, "● %-55s %s\n", clean(item.Name), clean(item.Updated))
|
|
619
|
-
}
|
|
620
|
-
return b.String()
|
|
621
|
-
case 5:
|
|
622
|
-
return "CURATED CAPABILITIES\n\nSkills: /goal, /loop, TDD, debugging, verification, security, release discipline\nMCP: Context7, Playwright, knowledge-graph memory\nScanners: Trivy, Gitleaks, OSV-Scanner, Semgrep"
|
|
623
|
-
case 6:
|
|
624
|
-
if m.logs == "" {
|
|
625
|
-
return "No log snapshot available."
|
|
626
|
-
}
|
|
627
|
-
return clean(m.logs)
|
|
628
739
|
default:
|
|
629
|
-
return
|
|
740
|
+
return m.settings()
|
|
630
741
|
}
|
|
631
742
|
}
|
|
632
743
|
|
|
@@ -636,6 +747,9 @@ func (m model) View() string {
|
|
|
636
747
|
width = 80
|
|
637
748
|
}
|
|
638
749
|
header := lipgloss.NewStyle().Bold(true).Foreground(accent).Render(strings.ToUpper(clean(m.factoryName))) + " " + lipgloss.NewStyle().Foreground(muted).Render(clean(m.purpose))
|
|
750
|
+
if selected := m.selected(); selected != nil {
|
|
751
|
+
header += " " + lipgloss.NewStyle().Foreground(blue).Render("/ "+clean(selected.Name))
|
|
752
|
+
}
|
|
639
753
|
tabValues := []string{}
|
|
640
754
|
for index, value := range tabs {
|
|
641
755
|
style := lipgloss.NewStyle().Padding(0, 1).Foreground(muted)
|
|
@@ -645,16 +759,21 @@ func (m model) View() string {
|
|
|
645
759
|
tabValues = append(tabValues, style.Render(fmt.Sprintf("%d %s", index+1, value)))
|
|
646
760
|
}
|
|
647
761
|
tabLine := lipgloss.JoinHorizontal(lipgloss.Top, tabValues...)
|
|
648
|
-
|
|
649
|
-
if height < 10 {
|
|
650
|
-
height = 10
|
|
651
|
-
}
|
|
652
|
-
body := m.body()
|
|
762
|
+
consoleHeight := 0
|
|
653
763
|
if m.commandOutput != "" {
|
|
654
|
-
|
|
764
|
+
consoleHeight = 7
|
|
765
|
+
}
|
|
766
|
+
mainHeight := m.height - 8 - consoleHeight
|
|
767
|
+
if mainHeight < 10 {
|
|
768
|
+
mainHeight = 10
|
|
655
769
|
}
|
|
656
|
-
|
|
657
|
-
|
|
770
|
+
sidebarWidth := 26
|
|
771
|
+
contentWidth := width - sidebarWidth - 5
|
|
772
|
+
if contentWidth < 50 {
|
|
773
|
+
contentWidth = 50
|
|
774
|
+
}
|
|
775
|
+
lines := strings.Split(m.body(), "\n")
|
|
776
|
+
visible := mainHeight - 2
|
|
658
777
|
maxScroll := len(lines) - visible
|
|
659
778
|
if maxScroll < 0 {
|
|
660
779
|
maxScroll = 0
|
|
@@ -667,17 +786,42 @@ func (m model) View() string {
|
|
|
667
786
|
if end > len(lines) {
|
|
668
787
|
end = len(lines)
|
|
669
788
|
}
|
|
670
|
-
|
|
671
|
-
|
|
789
|
+
sidebar := lipgloss.NewStyle().Width(sidebarWidth).Height(mainHeight).Border(lipgloss.RoundedBorder()).BorderForeground(border).Padding(1).Render(m.sidebar(mainHeight - 2))
|
|
790
|
+
content := lipgloss.NewStyle().Width(contentWidth).Height(mainHeight).Border(lipgloss.RoundedBorder()).BorderForeground(border).Padding(1, 2).Render(strings.Join(lines[scroll:end], "\n"))
|
|
791
|
+
main := lipgloss.JoinHorizontal(lipgloss.Top, sidebar, content)
|
|
792
|
+
console := ""
|
|
793
|
+
if m.commandOutput != "" {
|
|
794
|
+
consoleLines := strings.Split(clean(m.commandOutput), "\n")
|
|
795
|
+
maxConsoleScroll := len(consoleLines) - 4
|
|
796
|
+
if maxConsoleScroll < 0 {
|
|
797
|
+
maxConsoleScroll = 0
|
|
798
|
+
}
|
|
799
|
+
consoleScroll := m.consoleScroll
|
|
800
|
+
if consoleScroll > maxConsoleScroll {
|
|
801
|
+
consoleScroll = maxConsoleScroll
|
|
802
|
+
}
|
|
803
|
+
consoleEnd := consoleScroll + 4
|
|
804
|
+
if consoleEnd > len(consoleLines) {
|
|
805
|
+
consoleEnd = len(consoleLines)
|
|
806
|
+
}
|
|
807
|
+
console = lipgloss.NewStyle().Width(width-2).Height(5).Border(lipgloss.RoundedBorder()).BorderForeground(muted).Padding(0, 1).Render(fmt.Sprintf("CONSOLE · ctrl+↑/↓ scroll · ctrl+l clear [%d-%d/%d]\n%s", consoleScroll+1, consoleEnd, len(consoleLines), strings.Join(consoleLines[consoleScroll:consoleEnd], "\n")))
|
|
808
|
+
}
|
|
809
|
+
prompt := "Press : to enter a Factory command · n submit · i import · ? help · q quit"
|
|
672
810
|
if m.commandMode {
|
|
673
|
-
|
|
811
|
+
prompt = lipgloss.NewStyle().Foreground(accent).Bold(true).Render("› ") + clean(m.commandInput) + "█"
|
|
674
812
|
} else if m.loading {
|
|
675
|
-
|
|
813
|
+
prompt = "Refreshing Factory state…"
|
|
676
814
|
}
|
|
677
815
|
if m.err != nil && !m.commandMode {
|
|
678
|
-
|
|
816
|
+
prompt = lipgloss.NewStyle().Foreground(danger).Render(m.err.Error())
|
|
817
|
+
}
|
|
818
|
+
commandLine := lipgloss.NewStyle().Width(width-2).Height(1).Border(lipgloss.RoundedBorder()).BorderForeground(accent).Padding(0, 1).Render(prompt)
|
|
819
|
+
parts := []string{header, tabLine, main}
|
|
820
|
+
if console != "" {
|
|
821
|
+
parts = append(parts, console)
|
|
679
822
|
}
|
|
680
|
-
|
|
823
|
+
parts = append(parts, commandLine)
|
|
824
|
+
return lipgloss.JoinVertical(lipgloss.Left, parts...)
|
|
681
825
|
}
|
|
682
826
|
|
|
683
827
|
func main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factory-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Deploy a private autonomous coding-agent factory on Azure: isolated builders, testers, security reviewers, durable orchestration, multi-model routing, memory, cost controls, and gated GitHub pull requests.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|